use meters in config

This commit is contained in:
2026-06-05 19:54:39 +02:00
parent 4e3e3ac715
commit 7669245229
37 changed files with 265 additions and 231 deletions

View File

@@ -10,7 +10,7 @@ glyph = "L"
[module.weapon] [module.weapon]
damage_formula = "2" damage_formula = "2"
attack_range_formula = "5" attack_range_formula = "50"
attack_rate_formula = "2.0" attack_rate_formula = "2.0"
[[module]] [[module]]
@@ -24,7 +24,7 @@ fill_color = "#AACC44"
glyph = "Sv" glyph = "Sv"
[module.salvage] [module.salvage]
collection_range_formula = "50" collection_range_formula = "500"
cargo_capacity_formula = "10" cargo_capacity_formula = "10"
collection_rate_formula = "0.5" collection_rate_formula = "0.5"
@@ -40,4 +40,4 @@ glyph = "Rp"
[module.repair] [module.repair]
repair_rate_formula = "5 + x" repair_rate_formula = "5 + x"
repair_range_formula = "80" repair_range_formula = "800"

View File

@@ -16,14 +16,14 @@ cost_formula = "10"
hp_formula = "3" hp_formula = "3"
[ship.movement] [ship.movement]
speed_formula = "4" speed_formula = "40"
main_acceleration_formula = "8" main_acceleration_formula = "80"
maneuvering_acceleration_formula = "4" maneuvering_acceleration_formula = "40"
angular_acceleration_formula = "12.56" angular_acceleration_formula = "12.56"
max_rotation_speed_formula = "6.28" max_rotation_speed_formula = "6.28"
[ship.sensor] [ship.sensor]
sensor_range_formula = "15" sensor_range_formula = "150"
[ship.loot] [ship.loot]
scrap_drop = 2 scrap_drop = 2

View File

@@ -14,7 +14,7 @@ surface_mask = [
level = 1 level = 1
hp_formula = "300" hp_formula = "300"
damage_formula = "5" damage_formula = "5"
range_formula = "20" range_formula = "200"
fire_rate_formula = "1" fire_rate_formula = "1"
scrap_drop_formula = "10" scrap_drop_formula = "10"
@@ -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 = "20" range_formula = "200"
fire_rate_formula = "1.0 + 0.2*x" fire_rate_formula = "1.0 + 0.2*x"
scrap_drop_formula = "10 + 5*x" scrap_drop_formula = "10 + 5*x"

View File

@@ -3,7 +3,8 @@ height_tiles = 30
refund_percentage = 75 refund_percentage = 75
starting_building_blocks = 1000 starting_building_blocks = 1000
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 tile_size_m = 10
belt_speed_mps = 20
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20 departure_interval_seconds = 20

View File

@@ -22,7 +22,7 @@ fill_color = "#40A0FF"
glyph = "S" glyph = "S"
[module.sensor] [module.sensor]
added_sensor_range_formula = "10" added_sensor_range_formula = "100"
[[module]] [[module]]
id = "weapon_upgrade" id = "weapon_upgrade"
@@ -49,7 +49,7 @@ glyph = "L"
[module.weapon] [module.weapon]
damage_formula = "2" damage_formula = "2"
attack_range_formula = "5" attack_range_formula = "50"
attack_rate_formula = "2.0" attack_rate_formula = "2.0"
[[module]] [[module]]
@@ -63,7 +63,7 @@ fill_color = "#AACC44"
glyph = "Sv" glyph = "Sv"
[module.salvage] [module.salvage]
collection_range_formula = "50" collection_range_formula = "500"
cargo_capacity_formula = "10" cargo_capacity_formula = "10"
collection_rate_formula = "0.5" collection_rate_formula = "0.5"
@@ -79,4 +79,4 @@ glyph = "Rp"
[module.repair] [module.repair]
repair_rate_formula = "5 + x" repair_rate_formula = "5 + x"
repair_range_formula = "80" repair_range_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 = "200 + 5*x" speed_formula = "2000 + 50*x"
main_acceleration_formula = "100000" main_acceleration_formula = "1000000"
maneuvering_acceleration_formula = "100000" maneuvering_acceleration_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "200" sensor_range_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 = "120" speed_formula = "1200"
main_acceleration_formula = "100000" main_acceleration_formula = "1000000"
maneuvering_acceleration_formula = "100000" maneuvering_acceleration_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "300" sensor_range_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 = "110" speed_formula = "1100"
main_acceleration_formula = "100000" main_acceleration_formula = "1000000"
maneuvering_acceleration_formula = "100000" maneuvering_acceleration_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "250" sensor_range_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 = "130" speed_formula = "1300"
main_acceleration_formula = "100000" main_acceleration_formula = "1000000"
maneuvering_acceleration_formula = "100000" maneuvering_acceleration_formula = "1000000"
angular_acceleration_formula = "100000" angular_acceleration_formula = "100000"
max_rotation_speed_formula = "100000" max_rotation_speed_formula = "100000"
[ship.sensor] [ship.sensor]
sensor_range_formula = "250" sensor_range_formula = "2500"
[ship.loot] [ship.loot]
scrap_drop = 2 scrap_drop = 2

View File

@@ -14,7 +14,7 @@ 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 = "300 + 20*x" range_formula = "3000 + 200*x"
fire_rate_formula = "0.5 + 0.2*x" fire_rate_formula = "0.5 + 0.2*x"
scrap_drop_formula = "x" scrap_drop_formula = "x"
@@ -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 = "350 + 20*x" range_formula = "3500 + 200*x"
fire_rate_formula = "1.0 + 0.2*x" fire_rate_formula = "1.0 + 0.2*x"
scrap_drop_formula = "10 + 5*x" scrap_drop_formula = "10 + 5*x"

View File

@@ -3,7 +3,8 @@ height_tiles = 60
refund_percentage = 75 refund_percentage = 75
starting_building_blocks = 100 starting_building_blocks = 100
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 tile_size_m = 10
belt_speed_mps = 20
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20 departure_interval_seconds = 20

View File

@@ -123,6 +123,7 @@ void ArenaSimulation::placeStructures()
weapon.cooldownTicks = 0.0f; weapon.cooldownTicks = 0.0f;
weapon.currentTarget = std::nullopt; weapon.currentTarget = std::nullopt;
const double lv = static_cast<double>(entry.level); const double lv = static_cast<double>(entry.level);
const float tileSize = static_cast<float>(m_gameConfig.world.tileSize_m);
const std::vector<std::string>& mask = isEnemy const std::vector<std::string>& mask = isEnemy
? m_gameConfig.stations.enemyStation.surfaceMask ? m_gameConfig.stations.enemyStation.surfaceMask
@@ -134,8 +135,8 @@ void ArenaSimulation::placeStructures()
m_gameConfig.stations.playerStation.hpFormula.evaluate(lv)); m_gameConfig.stations.playerStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>( weapon.damage = static_cast<float>(
m_gameConfig.stations.playerStation.damageFormula.evaluate(lv)); m_gameConfig.stations.playerStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>( weapon.range_tiles = static_cast<float>(
m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)); m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)) / tileSize;
weapon.fireRateHz = static_cast<float>( weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv)); m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv));
} }
@@ -145,8 +146,8 @@ void ArenaSimulation::placeStructures()
m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv)); m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv));
weapon.damage = static_cast<float>( weapon.damage = static_cast<float>(
m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv)); m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv));
weapon.range = static_cast<float>( weapon.range_tiles = static_cast<float>(
m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)); m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)) / tileSize;
weapon.fireRateHz = static_cast<float>( weapon.fireRateHz = static_cast<float>(
m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv)); m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv));
} }

View File

@@ -264,7 +264,8 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage")); cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks")); cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
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.beltSpeedTilesPerSecond = requireDouble(tbl["world"]["belt_speed_tiles_per_second"], file, "world.belt_speed_tiles_per_second"); 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.tunnelMaxDistance = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance")); cfg.tunnelMaxDistance = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance"));
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");

View File

@@ -45,7 +45,8 @@ struct WorldConfig
int refundPercentage; // REQ-BLD-DEMOLISH int refundPercentage; // REQ-BLD-DEMOLISH
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
double beltSpeedTilesPerSecond; // REQ-GW-BELT-SPEED 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)
int tunnelMaxDistance; // REQ-BLD-TUNNEL-PAIR int tunnelMaxDistance; // REQ-BLD-TUNNEL-PAIR
double departureIntervalSeconds; // REQ-SHP-RALLY double departureIntervalSeconds; // REQ-SHP-RALLY

View File

@@ -39,9 +39,9 @@ void EntityAdmin::clear()
} }
entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp, entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
float maxSpeedPerTick, float mainAccelPerTick, float maxSpeed_tpt, float mainAcceleration_tptt,
float maneuveringAccelPerTick, float angularAccelPerTick, float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
float maxRotationSpeedPerTick, float sensorRange, float maxRotationSpeed_rpt, float sensorRange_tiles,
int level, const std::string& schematicId, bool isEnemy) int level, const std::string& schematicId, bool isEnemy)
{ {
entt::entity entity = createEntity(); entt::entity entity = createEntity();
@@ -50,17 +50,17 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
add<FactionComponent>(entity, FactionComponent{isEnemy}); add<FactionComponent>(entity, FactionComponent{isEnemy});
add<FacingComponent>(entity, FacingComponent{0.0f}); add<FacingComponent>(entity, FacingComponent{0.0f});
add<DynamicBodyComponent>(entity, DynamicBodyComponent{ add<DynamicBodyComponent>(entity, DynamicBodyComponent{
maxSpeedPerTick, maxSpeed_tpt,
mainAccelPerTick, mainAcceleration_tptt,
maneuveringAccelPerTick, maneuveringAcceleration_tptt,
angularAccelPerTick, maxAngularAcceleration_rptt,
maxRotationSpeedPerTick, maxRotationSpeed_rpt,
QVector2D(0.0f, 0.0f), // velocity QVector2D(0.0f, 0.0f), // velocity_tpt
0.0f, // angularVelocity 0.0f, // angularVelocity_rpt
QVector2D(0.0f, 0.0f), // linearAcceleration QVector2D(0.0f, 0.0f), // linearAcceleration_tptt
0.0f // angularAcceleration 0.0f // angularAcceleration_rptt
}); });
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange}); add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
add<ShipIdentityComponent>(entity, ShipIdentityComponent{level, schematicId}); add<ShipIdentityComponent>(entity, ShipIdentityComponent{level, schematicId});
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)}); add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
return entity; return entity;

View File

@@ -53,9 +53,9 @@ public:
// -- Factory methods ---------------------------------------------------- // -- Factory methods ----------------------------------------------------
entt::entity spawnShip(QVector2D position, float hp, float maxHp, entt::entity spawnShip(QVector2D position, float hp, float maxHp,
float maxSpeedPerTick, float mainAccelPerTick, float maxSpeed_tpt, float mainAcceleration_tptt,
float maneuveringAccelPerTick, float angularAccelPerTick, float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt,
float maxRotationSpeedPerTick, float sensorRange, float maxRotationSpeed_rpt, float sensorRange_tiles,
int level, const std::string& schematicId, bool isEnemy); int level, const std::string& schematicId, bool isEnemy);
entt::entity spawnStation(QPoint anchor, QSize footprint, entt::entity spawnStation(QPoint anchor, QSize footprint,

View File

@@ -4,18 +4,18 @@
struct DynamicBodyComponent struct DynamicBodyComponent
{ {
// --- dynamics parameters (formerly ShipDynamics) --- // --- dynamics parameters ---
float maxSpeedPerTick; float maxSpeed_tpt; // tiles/tick
float mainAccelerationPerTick; float mainAcceleration_tptt; // tiles/tick²
float maneuveringAccelerationPerTick; float maneuveringAcceleration_tptt; // tiles/tick²
float angularAccelerationPerTick; float maxAngularAcceleration_rptt; // rad/tick²
float maxRotationSpeedPerTick; float maxRotationSpeed_rpt; // rad/tick
// --- integrated state --- // --- integrated state ---
QVector2D velocity; QVector2D velocity_tpt; // tiles/tick
float angularVelocity; float angularVelocity_rpt; // rad/tick
// --- written each tick by MovementIntentSystem, consumed by DynamicBodySystem --- // --- written each tick by MovementIntentSystem, consumed by DynamicBodySystem ---
QVector2D linearAcceleration; QVector2D linearAcceleration_tptt; // tiles/tick²
float angularAcceleration; float angularAcceleration_rptt; // rad/tick²
}; };

View File

@@ -7,5 +7,5 @@
struct RepairBehaviorComponent struct RepairBehaviorComponent
{ {
std::optional<entt::entity> currentTarget; std::optional<entt::entity> currentTarget;
float maxRepairRange = 0.0f; float maxRepairRange_tiles = 0.0f;
}; };

View File

@@ -7,6 +7,6 @@
struct RepairToolComponent struct RepairToolComponent
{ {
float ratePerTick; float ratePerTick;
float range; float range_tiles;
std::optional<entt::entity> currentTarget; std::optional<entt::entity> currentTarget;
}; };

View File

@@ -10,5 +10,5 @@ struct SalvageBehaviorComponent
{ {
std::optional<QVector2D> scrapTarget; std::optional<QVector2D> scrapTarget;
BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay
float maxCollectionRange = 0.0f; float maxCollectionRange_tiles = 0.0f;
}; };

View File

@@ -4,7 +4,7 @@ struct SalvageCargoComponent
{ {
int capacity; int capacity;
int current; int current;
float collectionRange; float collectionRange_tiles;
int collectionIntervalTicks; int collectionIntervalTicks;
int cooldownTicksRemaining; int cooldownTicksRemaining;
}; };

View File

@@ -2,5 +2,5 @@
struct SensorRangeComponent struct SensorRangeComponent
{ {
float value; float value_tiles;
}; };

View File

@@ -7,7 +7,7 @@
struct WeaponComponent struct WeaponComponent
{ {
float damage; float damage;
float range; float range_tiles;
float fireRateHz; float fireRateHz;
float cooldownTicks; float cooldownTicks;
std::optional<entt::entity> currentTarget; std::optional<entt::entity> currentTarget;

View File

@@ -132,7 +132,7 @@ void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSyst
PositionComponent& pos, FactionComponent& faction, PositionComponent& pos, FactionComponent& faction,
SensorRangeComponent& sensor, MovementIntentComponent& intent) SensorRangeComponent& sensor, MovementIntentComponent& intent)
{ {
const float range = sensor.value; const float range = sensor.value_tiles;
// Validate current target. // Validate current target.
bool targetValid = false; bool targetValid = false;
@@ -251,7 +251,7 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
bool enemyNearby = false; bool enemyNearby = false;
for (const EnemyInfo& enemy : enemies) for (const EnemyInfo& enemy : enemies)
{ {
if ((enemy.position - pos.value).length() <= sensor.value) if ((enemy.position - pos.value).length() <= sensor.value_tiles)
{ {
enemyNearby = true; enemyNearby = true;
break; break;
@@ -285,7 +285,7 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings)
if (!targetValid) if (!targetValid)
{ {
rb.currentTarget = std::nullopt; rb.currentTarget = std::nullopt;
float bestDist = sensor.value; float bestDist = sensor.value_tiles;
for (const RepairableInfo& r : repairables) for (const RepairableInfo& r : repairables)
{ {
@@ -355,7 +355,7 @@ void AiSystem::tickRepairTools(EntityAdmin& admin)
const float dist = const float dist =
(admin.get<PositionComponent>(preferred).value (admin.get<PositionComponent>(preferred).value
- ownerPos.value).length(); - ownerPos.value).length();
if (th.hp > 0.0f && th.hp < th.maxHp && dist <= rt.range) if (th.hp > 0.0f && th.hp < th.maxHp && dist <= rt.range_tiles)
{ {
rt.currentTarget = rb.currentTarget; rt.currentTarget = rb.currentTarget;
th.hp = std::min(th.hp + rt.ratePerTick, th.maxHp); th.hp = std::min(th.hp + rt.ratePerTick, th.maxHp);
@@ -366,7 +366,7 @@ void AiSystem::tickRepairTools(EntityAdmin& admin)
// Preferred target unavailable; scan for nearest damaged friendly in range. // Preferred target unavailable; scan for nearest damaged friendly in range.
rt.currentTarget = std::nullopt; rt.currentTarget = std::nullopt;
float bestDist = rt.range; float bestDist = rt.range_tiles;
for (const RepairableInfo& r : repairables) for (const RepairableInfo& r : repairables)
{ {
if (r.isEnemy) { continue; } if (r.isEnemy) { continue; }
@@ -441,7 +441,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
PositionComponent& pos, PositionComponent& pos,
SensorRangeComponent& sensor, MovementIntentComponent& intent) SensorRangeComponent& sensor, MovementIntentComponent& intent)
{ {
const float collectRange = salvageBehavior.maxCollectionRange; const float collectRange = salvageBehavior.maxCollectionRange_tiles;
const AggregatedCargo& cargoState = cargoByShip[e]; const AggregatedCargo& cargoState = cargoByShip[e];
// Assign nearest SalvageBay if needed. // Assign nearest SalvageBay if needed.
@@ -530,7 +530,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
} }
for (const ScrapInfo& si : allScrap) for (const ScrapInfo& si : allScrap)
{ {
if ((si.position - pos.value).length() > c.collectionRange) { continue; } if ((si.position - pos.value).length() > c.collectionRange_tiles) { continue; }
if (scraps.consume(si.entity)) if (scraps.consume(si.entity))
{ {
++c.current; ++c.current;
@@ -555,7 +555,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps,
} }
else else
{ {
float bestDist = sensor.value; float bestDist = sensor.value_tiles;
std::optional<QVector2D> bestPos; std::optional<QVector2D> bestPos;
for (const ScrapInfo& si : allScrap) for (const ScrapInfo& si : allScrap)
{ {

View File

@@ -69,7 +69,7 @@ void CombatSystem::resolveWeapon(
{ {
const float distanceSquared = const float distanceSquared =
(ownPos.value - admin.get<PositionComponent>(t).value).lengthSquared(); (ownPos.value - admin.get<PositionComponent>(t).value).lengthSquared();
if (distanceSquared > weapon.range * weapon.range) if (distanceSquared > weapon.range_tiles * weapon.range_tiles)
{ {
weapon.currentTarget = std::nullopt; weapon.currentTarget = std::nullopt;
} }
@@ -81,8 +81,8 @@ void CombatSystem::resolveWeapon(
if (!weapon.currentTarget) if (!weapon.currentTarget)
{ {
const float acquisitionRange = admin.hasAll<SensorRangeComponent>(shipEntity) const float acquisitionRange = admin.hasAll<SensorRangeComponent>(shipEntity)
? admin.get<SensorRangeComponent>(shipEntity).value ? admin.get<SensorRangeComponent>(shipEntity).value_tiles
: weapon.range; : weapon.range_tiles;
float bestDistanceSquared = acquisitionRange * acquisitionRange; float bestDistanceSquared = acquisitionRange * acquisitionRange;
admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>( admin.forEach<ShipIdentityComponent, PositionComponent, FactionComponent>(
[&](entt::entity candidate, const ShipIdentityComponent& /*si*/, [&](entt::entity candidate, const ShipIdentityComponent& /*si*/,

View File

@@ -28,27 +28,27 @@ void DynamicBodySystem::tick(EntityAdmin& admin)
DynamicBodyComponent& body) DynamicBodyComponent& body)
{ {
// Integrate angular velocity, clamp to max rotation speed, then advance facing. // Integrate angular velocity, clamp to max rotation speed, then advance facing.
body.angularVelocity += body.angularAcceleration; body.angularVelocity_rpt += body.angularAcceleration_rptt;
body.angularVelocity = std::max(-body.maxRotationSpeedPerTick, body.angularVelocity_rpt = std::max(-body.maxRotationSpeed_rpt,
std::min(body.angularVelocity, std::min(body.angularVelocity_rpt,
body.maxRotationSpeedPerTick)); body.maxRotationSpeed_rpt));
facing.radians = wrapAngle(facing.radians + body.angularVelocity); facing.radians = wrapAngle(facing.radians + body.angularVelocity_rpt);
// Integrate linear velocity and cap to max speed. // Integrate linear velocity and cap to max speed.
body.velocity += body.linearAcceleration; body.velocity_tpt += body.linearAcceleration_tptt;
const float speed = body.velocity.length(); const float speed = body.velocity_tpt.length();
if (speed > body.maxSpeedPerTick) if (speed > body.maxSpeed_tpt)
{ {
body.velocity = body.velocity.normalized() * body.maxSpeedPerTick; body.velocity_tpt = body.velocity_tpt.normalized() * body.maxSpeed_tpt;
} }
// Advance position. // Advance position.
pos.value += body.velocity; pos.value += body.velocity_tpt;
// Reset per-tick fields so stale values don't linger if the intent // Reset per-tick fields so stale values don't linger if the intent
// system is skipped for this entity in a future tick. // system is skipped for this entity in a future tick.
body.linearAcceleration = QVector2D(0.0f, 0.0f); body.linearAcceleration_tptt = QVector2D(0.0f, 0.0f);
body.angularAcceleration = 0.0f; body.angularAcceleration_rptt = 0.0f;
}); });
} }

View File

@@ -32,16 +32,16 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
if (intent.priority == 0) if (intent.priority == 0)
{ {
// No movement intent: brake using available thrust. // No movement intent: brake using available thrust.
const float linearBraking = std::min(body.velocity.length(), const float linearBraking = std::min(body.velocity_tpt.length(),
body.maneuveringAccelerationPerTick); body.maneuveringAcceleration_tptt);
body.linearAcceleration = (body.velocity.length() > 0.0001f) body.linearAcceleration_tptt = (body.velocity_tpt.length() > 0.0001f)
? -body.velocity.normalized() * linearBraking ? -body.velocity_tpt.normalized() * linearBraking
: QVector2D(0.0f, 0.0f); : QVector2D(0.0f, 0.0f);
const float angBraking = std::min(std::abs(body.angularVelocity), const float angBraking = std::min(std::abs(body.angularVelocity_rpt),
body.angularAccelerationPerTick); body.maxAngularAcceleration_rptt);
body.angularAcceleration = body.angularAcceleration_rptt =
(body.angularVelocity >= 0.0f) ? -angBraking : angBraking; (body.angularVelocity_rpt >= 0.0f) ? -angBraking : angBraking;
return; return;
} }
@@ -52,8 +52,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
{ {
// Already at target: no new thrust. The ship drifts; it will // Already at target: no new thrust. The ship drifts; it will
// re-approach next tick once it has moved away. // re-approach next tick once it has moved away.
body.linearAcceleration = QVector2D(0.0f, 0.0f); body.linearAcceleration_tptt = QVector2D(0.0f, 0.0f);
body.angularAcceleration = 0.0f; body.angularAcceleration_rptt = 0.0f;
return; return;
} }
@@ -62,11 +62,11 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
const float desiredAngle = std::atan2(delta.y(), delta.x()); const float desiredAngle = std::atan2(delta.y(), delta.x());
const float angleDiff = wrapAngle(desiredAngle - facing.radians); const float angleDiff = wrapAngle(desiredAngle - facing.radians);
const float rotDelta = std::max(-body.angularAccelerationPerTick, const float rotDelta = std::max(-body.maxAngularAcceleration_rptt,
std::min(angleDiff, std::min(angleDiff,
body.angularAccelerationPerTick)); body.maxAngularAcceleration_rptt));
float newAngVel = body.angularVelocity + rotDelta; float newAngVel = body.angularVelocity_rpt + rotDelta;
// Overshoot prevention: if the accumulated angular velocity already // Overshoot prevention: if the accumulated angular velocity already
// exceeds the remaining angle, snap it to exactly that angle so the // exceeds the remaining angle, snap it to exactly that angle so the
@@ -77,8 +77,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
newAngVel = angleDiff; newAngVel = angleDiff;
} }
body.angularAcceleration = newAngVel - body.angularVelocity; body.angularAcceleration_rptt = newAngVel - body.angularVelocity_rpt;
// DynamicBodySystem applies the clamp to maxRotationSpeedPerTick after // DynamicBodySystem applies the clamp to maxRotationSpeed_rpt after
// integrating, so we do not clamp here. // integrating, so we do not clamp here.
// --- Linear acceleration --- // --- Linear acceleration ---
@@ -90,22 +90,22 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
const QVector2D facingVec(std::cos(projectedRadians), const QVector2D facingVec(std::cos(projectedRadians),
std::sin(projectedRadians)); std::sin(projectedRadians));
const float manAccel = body.maneuveringAccelerationPerTick; const float manAccel = body.maneuveringAcceleration_tptt;
const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick) const float stoppingDist = (body.maxSpeed_tpt * body.maxSpeed_tpt)
/ (2.0f * manAccel); / (2.0f * manAccel);
// Cap to dist so the ship never overshoots the target in a single tick. // Cap to dist so the ship never overshoots the target in a single tick.
const float baseDesiredSpeed = (dist <= stoppingDist) const float baseDesiredSpeed = (dist <= stoppingDist)
? std::sqrt(2.0f * manAccel * dist) ? std::sqrt(2.0f * manAccel * dist)
: body.maxSpeedPerTick; : body.maxSpeed_tpt;
const float desiredSpeed = std::min(dist, baseDesiredSpeed); const float desiredSpeed = std::min(dist, baseDesiredSpeed);
const QVector2D desiredVel = delta.normalized() * desiredSpeed; const QVector2D desiredVel = delta.normalized() * desiredSpeed;
const QVector2D velError = desiredVel - body.velocity; const QVector2D velError = desiredVel - body.velocity_tpt;
const float mainAligned = std::max(0.0f, const float mainAligned = std::max(0.0f,
QVector2D::dotProduct(velError, facingVec)); QVector2D::dotProduct(velError, facingVec));
const float mainApplied = std::min(mainAligned, const float mainApplied = std::min(mainAligned,
body.mainAccelerationPerTick); body.mainAcceleration_tptt);
const QVector2D mainDelta = facingVec * mainApplied; const QVector2D mainDelta = facingVec * mainApplied;
const QVector2D remaining = velError - mainDelta; const QVector2D remaining = velError - mainDelta;
@@ -114,7 +114,7 @@ void MovementIntentSystem::tick(EntityAdmin& admin)
? remaining.normalized() * manAccel ? remaining.normalized() * manAccel
: remaining; : remaining;
body.linearAcceleration = mainDelta + maneuverDelta; body.linearAcceleration_tptt = mainDelta + maneuverDelta;
}); });
} }

View File

@@ -62,30 +62,32 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
const double x = static_cast<double>(level); const double x = static_cast<double>(level);
const float tickRate = static_cast<float>(kTickRateHz); const float tickRate = static_cast<float>(kTickRateHz);
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
float hp = static_cast<float>(def->health.hpFormula.evaluate(x)); float hp = static_cast<float>(def->health.hpFormula.evaluate(x));
float maxHp = hp; float maxHp = hp;
float maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x)) float maxSpeed_tpt = static_cast<float>(def->movement.speedFormula.evaluate(x))
/ tickRate; / tileSize / tickRate;
float mainAccelPerTick = static_cast<float>( float mainAcceleration_tptt = static_cast<float>(
def->movement.mainAccelerationFormula.evaluate(x)) def->movement.mainAccelerationFormula.evaluate(x))
/ tickRate; / tileSize / tickRate;
float maneuveringAccelPerTick = static_cast<float>( float maneuveringAcceleration_tptt = static_cast<float>(
def->movement.maneuveringAccelerationFormula.evaluate(x)) def->movement.maneuveringAccelerationFormula.evaluate(x))
/ tickRate; / tileSize / tickRate;
float angularAccelPerTick = static_cast<float>( float maxAngularAcceleration_rptt = static_cast<float>(
def->movement.angularAccelerationFormula.evaluate(x)) def->movement.angularAccelerationFormula.evaluate(x))
/ tickRate; / tickRate;
float maxRotationSpeedPerTick = static_cast<float>( float maxRotationSpeed_rpt = static_cast<float>(
def->movement.maxRotationSpeedFormula.evaluate(x)) def->movement.maxRotationSpeedFormula.evaluate(x))
/ tickRate; / tickRate;
float sensorRange = static_cast<float>( float sensorRange_tiles = static_cast<float>(
def->sensor.sensorRangeFormula.evaluate(x)); def->sensor.sensorRangeFormula.evaluate(x))
/ tileSize;
entt::entity entity = m_admin.spawnShip( entt::entity entity = m_admin.spawnShip(
position, hp, maxHp, position, hp, maxHp,
maxSpeedPerTick, mainAccelPerTick, maneuveringAccelPerTick, maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt,
angularAccelPerTick, maxRotationSpeedPerTick, sensorRange, maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles,
level, schematicId, isEnemy); level, schematicId, isEnemy);
// Determine module list: configured layout takes precedence over default. // Determine module list: configured layout takes precedence over default.
@@ -109,8 +111,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
WeaponComponent w; WeaponComponent w;
w.damage = static_cast<float>( w.damage = static_cast<float>(
modDef->weaponCapability->damageFormula.evaluate(mx)); modDef->weaponCapability->damageFormula.evaluate(mx));
w.range = static_cast<float>( w.range_tiles = static_cast<float>(
modDef->weaponCapability->attackRangeFormula.evaluate(mx)); modDef->weaponCapability->attackRangeFormula.evaluate(mx)) / tileSize;
w.fireRateHz = static_cast<float>( w.fireRateHz = static_cast<float>(
modDef->weaponCapability->attackRateFormula.evaluate(mx)); modDef->weaponCapability->attackRateFormula.evaluate(mx));
w.cooldownTicks = 0.0f; w.cooldownTicks = 0.0f;
@@ -128,8 +130,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
cargo.capacity = static_cast<int>( cargo.capacity = static_cast<int>(
modDef->salvageCapability->cargoCapacityFormula.evaluate(mx)); modDef->salvageCapability->cargoCapacityFormula.evaluate(mx));
cargo.current = 0; cargo.current = 0;
cargo.collectionRange = static_cast<float>( cargo.collectionRange_tiles = static_cast<float>(
modDef->salvageCapability->collectionRangeFormula.evaluate(mx)); modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize;
const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx); const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx);
cargo.collectionIntervalTicks = (rate > 0.0) cargo.collectionIntervalTicks = (rate > 0.0)
? static_cast<int>(kTickRateHz / rate + 0.5) ? static_cast<int>(kTickRateHz / rate + 0.5)
@@ -148,8 +150,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
rt.ratePerTick = static_cast<float>( rt.ratePerTick = static_cast<float>(
modDef->repairCapability->repairRateFormula.evaluate(mx)) modDef->repairCapability->repairRateFormula.evaluate(mx))
/ static_cast<float>(kTickRateHz); / static_cast<float>(kTickRateHz);
rt.range = static_cast<float>( rt.range_tiles = static_cast<float>(
modDef->repairCapability->repairRangeFormula.evaluate(mx)); modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize;
rt.currentTarget = std::nullopt; rt.currentTarget = std::nullopt;
entt::entity child = m_admin.createModuleEntity(); entt::entity child = m_admin.createModuleEntity();
@@ -207,6 +209,26 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
} }
} }
// 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 char* const kRangeStats[] = {
"sensor_range", "attack_range", "collection_range", "repair_range"
};
std::map<std::string, std::pair<double, double>>* allModMaps[] = {
&hullMods, &weaponMods, &salvageMods, &repairMods
};
for (const char* stat : kRangeStats)
{
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;
}
}
}
// 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)
@@ -228,12 +250,12 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
applyMod(health.maxHp, "hp", hullMods); applyMod(health.maxHp, "hp", hullMods);
health.hp = health.maxHp; health.hp = health.maxHp;
applyMod(dynamics.maxSpeedPerTick, "speed", hullMods); applyMod(dynamics.maxSpeed_tpt, "speed", hullMods);
applyMod(dynamics.mainAccelerationPerTick, "main_acceleration", hullMods); applyMod(dynamics.mainAcceleration_tptt, "main_acceleration", hullMods);
applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration", hullMods); applyMod(dynamics.maneuveringAcceleration_tptt, "maneuvering_acceleration", hullMods);
applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration", hullMods); applyMod(dynamics.maxAngularAcceleration_rptt, "angular_acceleration", hullMods);
applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed", hullMods); applyMod(dynamics.maxRotationSpeed_rpt, "max_rotation_speed", hullMods);
applyMod(sensor.value, "sensor_range", hullMods); applyMod(sensor.value_tiles, "sensor_range", hullMods);
} }
// Apply weapon modifiers to each weapon child. // Apply weapon modifiers to each weapon child.
@@ -241,7 +263,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
{ {
WeaponComponent& w = m_admin.get<WeaponComponent>(child); WeaponComponent& w = m_admin.get<WeaponComponent>(child);
applyMod(w.damage, "damage", weaponMods); applyMod(w.damage, "damage", weaponMods);
applyMod(w.range, "attack_range", weaponMods); applyMod(w.range_tiles, "attack_range", weaponMods);
applyMod(w.fireRateHz, "attack_rate", weaponMods); applyMod(w.fireRateHz, "attack_rate", weaponMods);
} }
@@ -249,7 +271,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
for (entt::entity child : salvageChildren) for (entt::entity child : salvageChildren)
{ {
SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child); SalvageCargoComponent& c = m_admin.get<SalvageCargoComponent>(child);
float fRange = c.collectionRange; float fRange = c.collectionRange_tiles;
float fCapacity = static_cast<float>(c.capacity); float fCapacity = static_cast<float>(c.capacity);
// Apply rate modifier: compute rate from interval, apply multiplier, convert back. // Apply rate modifier: compute rate from interval, apply multiplier, convert back.
float fRate = (c.collectionIntervalTicks > 0) float fRate = (c.collectionIntervalTicks > 0)
@@ -258,7 +280,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
applyMod(fRange, "collection_range", salvageMods); applyMod(fRange, "collection_range", salvageMods);
applyMod(fCapacity, "cargo_capacity", salvageMods); applyMod(fCapacity, "cargo_capacity", salvageMods);
applyMod(fRate, "collection_rate", salvageMods); applyMod(fRate, "collection_rate", salvageMods);
c.collectionRange = fRange; c.collectionRange_tiles = fRange;
c.capacity = static_cast<int>(fCapacity + 0.5f); c.capacity = static_cast<int>(fCapacity + 0.5f);
c.collectionIntervalTicks = (fRate > 0.0f) c.collectionIntervalTicks = (fRate > 0.0f)
? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f) ? static_cast<int>(static_cast<float>(kTickRateHz) / fRate + 0.5f)
@@ -270,7 +292,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
{ {
RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child); RepairToolComponent& rt = m_admin.get<RepairToolComponent>(child);
applyMod(rt.ratePerTick, "repair_rate", repairMods); applyMod(rt.ratePerTick, "repair_rate", repairMods);
applyMod(rt.range, "repair_range", repairMods); applyMod(rt.range_tiles, "repair_range", repairMods);
} }
// --- Pass 3: attach behavior components based on capability presence ----- // --- Pass 3: attach behavior components based on capability presence -----
@@ -292,14 +314,14 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
float maxCollRange = 0.0f; float maxCollRange = 0.0f;
for (entt::entity child : salvageChildren) for (entt::entity child : salvageChildren)
{ {
const float r = m_admin.get<SalvageCargoComponent>(child).collectionRange; const float r = m_admin.get<SalvageCargoComponent>(child).collectionRange_tiles;
if (r > maxCollRange) { maxCollRange = r; } if (r > maxCollRange) { maxCollRange = r; }
} }
SalvageBehaviorComponent sb; SalvageBehaviorComponent sb;
sb.scrapTarget = std::nullopt; sb.scrapTarget = std::nullopt;
sb.deliveryBay = kInvalidBuildingId; sb.deliveryBay = kInvalidBuildingId;
sb.maxCollectionRange = maxCollRange; sb.maxCollectionRange_tiles = maxCollRange;
m_admin.addComponent<SalvageBehaviorComponent>(entity, sb); m_admin.addComponent<SalvageBehaviorComponent>(entity, sb);
} }
@@ -308,13 +330,13 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
float maxRepairRange = 0.0f; float maxRepairRange = 0.0f;
for (entt::entity child : repairChildren) for (entt::entity child : repairChildren)
{ {
const float r = m_admin.get<RepairToolComponent>(child).range; const float r = m_admin.get<RepairToolComponent>(child).range_tiles;
if (r > maxRepairRange) { maxRepairRange = r; } if (r > maxRepairRange) { maxRepairRange = r; }
} }
RepairBehaviorComponent rb; RepairBehaviorComponent rb;
rb.currentTarget = std::nullopt; rb.currentTarget = std::nullopt;
rb.maxRepairRange = maxRepairRange; rb.maxRepairRange_tiles = maxRepairRange;
m_admin.addComponent<RepairBehaviorComponent>(entity, rb); m_admin.addComponent<RepairBehaviorComponent>(entity, rb);
} }

View File

@@ -47,8 +47,8 @@ QPointF BeltSystem::slotWorldPos(QPoint tile, Rotation dir, double progress)
// Construction / placement // Construction / placement
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
BeltSystem::BeltSystem(double beltSpeedTilesPerSecond) BeltSystem::BeltSystem(double beltSpeed_tps)
: m_progressPerTick(beltSpeedTilesPerSecond * kTickDurationSeconds) : m_progressPerTick_tpt(beltSpeed_tps * kTickDurationSeconds)
{ {
} }
@@ -414,7 +414,7 @@ void BeltSystem::advanceProgress()
for (std::size_t i = 0; i < bt.itemSlots.size(); ++i) for (std::size_t i = 0; i < bt.itemSlots.size(); ++i)
{ {
bt.itemSlots[i].progress += m_progressPerTick; bt.itemSlots[i].progress += m_progressPerTick_tpt;
// Absolute cap: slot i cannot exceed 1.0 - i * 0.25. // Absolute cap: slot i cannot exceed 1.0 - i * 0.25.
const double absoluteCap = 1.0 - i * 0.25; const double absoluteCap = 1.0 - i * 0.25;
@@ -442,7 +442,7 @@ void BeltSystem::advanceProgress()
for (std::size_t i = 0; i < st.back.size(); ++i) for (std::size_t i = 0; i < st.back.size(); ++i)
{ {
st.back[i].progress += m_progressPerTick; st.back[i].progress += m_progressPerTick_tpt;
const double absoluteCap = 0.5 - i * 0.25; const double absoluteCap = 0.5 - i * 0.25;
if (st.back[i].progress > absoluteCap) if (st.back[i].progress > absoluteCap)
{ {
@@ -465,7 +465,7 @@ void BeltSystem::advanceProgress()
if (st.frontA) if (st.frontA)
{ {
st.frontA->progress += m_progressPerTick; st.frontA->progress += m_progressPerTick_tpt;
if (st.frontA->progress > 1.0) if (st.frontA->progress > 1.0)
{ {
st.frontA->progress = 1.0; st.frontA->progress = 1.0;
@@ -474,7 +474,7 @@ void BeltSystem::advanceProgress()
if (st.frontB) if (st.frontB)
{ {
st.frontB->progress += m_progressPerTick; st.frontB->progress += m_progressPerTick_tpt;
if (st.frontB->progress > 1.0) if (st.frontB->progress > 1.0)
{ {
st.frontB->progress = 1.0; st.frontB->progress = 1.0;
@@ -492,7 +492,7 @@ void BeltSystem::advanceTunnelProgress()
for (std::size_t i = 0; i < te.itemSlots.size(); ++i) for (std::size_t i = 0; i < te.itemSlots.size(); ++i)
{ {
te.itemSlots[i].progress += m_progressPerTick; te.itemSlots[i].progress += m_progressPerTick_tpt;
const double absoluteCap = 1.0 - i * 0.25; const double absoluteCap = 1.0 - i * 0.25;
if (te.itemSlots[i].progress > absoluteCap) if (te.itemSlots[i].progress > absoluteCap)
@@ -518,7 +518,7 @@ void BeltSystem::advanceTunnelProgress()
for (std::size_t i = 0; i < tx.itemSlots.size(); ++i) for (std::size_t i = 0; i < tx.itemSlots.size(); ++i)
{ {
tx.itemSlots[i].progress += m_progressPerTick; tx.itemSlots[i].progress += m_progressPerTick_tpt;
const double absoluteCap = 1.0 - i * 0.25; const double absoluteCap = 1.0 - i * 0.25;
if (tx.itemSlots[i].progress > absoluteCap) if (tx.itemSlots[i].progress > absoluteCap)
@@ -542,7 +542,7 @@ void BeltSystem::advanceTunnelProgress()
for (std::size_t i = 0; i < link.items.size(); ++i) for (std::size_t i = 0; i < link.items.size(); ++i)
{ {
TunnelTransitItem& ti = link.items[i]; TunnelTransitItem& ti = link.items[i];
ti.progress += m_progressPerTick; ti.progress += m_progressPerTick_tpt;
if (ti.progress > link.length) if (ti.progress > link.length)
{ {
ti.progress = link.length; ti.progress = link.length;

View File

@@ -31,7 +31,7 @@ struct VisualItem
class BeltSystem class BeltSystem
{ {
public: public:
explicit BeltSystem(double beltSpeedTilesPerSecond); explicit BeltSystem(double beltSpeed_tps);
// -- Placement ----------------------------------------------------------- // -- Placement -----------------------------------------------------------
// Register a new belt tile. Any items already on this tile are cleared. // Register a new belt tile. Any items already on this tile are cleared.
@@ -170,7 +170,7 @@ private:
std::vector<TunnelTransitItem> items; // front (highest progress) to back std::vector<TunnelTransitItem> items; // front (highest progress) to back
}; };
double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz double m_progressPerTick_tpt; // beltSpeed_tps / kTickRateHz
std::map<std::pair<int, int>, BeltTile> m_belts; std::map<std::pair<int, int>, BeltTile> m_belts;
std::map<std::pair<int, int>, SplitterTile> m_splitters; std::map<std::pair<int, int>, SplitterTile> m_splitters;

View File

@@ -33,7 +33,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
, m_hqProxyEntity(entt::null) , m_hqProxyEntity(entt::null)
, m_playerStation1Entity(entt::null) , m_playerStation1Entity(entt::null)
, m_playerStation2Entity(entt::null) , m_playerStation2Entity(entt::null)
, m_beltSystem(m_config.world.beltSpeedTilesPerSecond) , m_beltSystem(m_config.world.beltSpeed_tps)
{ {
m_currentEnemyStationEntities[0] = entt::null; m_currentEnemyStationEntities[0] = entt::null;
m_currentEnemyStationEntities[1] = entt::null; m_currentEnemyStationEntities[1] = entt::null;
@@ -110,7 +110,7 @@ void Simulation::reset(unsigned int seed)
m_schematicDropEvents.clear(); m_schematicDropEvents.clear();
m_admin.clear(); m_admin.clear();
m_beltSystem = BeltSystem(m_config.world.beltSpeedTilesPerSecond); m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
m_buildingSystem = std::make_unique<BuildingSystem>( m_buildingSystem = std::make_unique<BuildingSystem>(
m_config, m_config,
m_beltSystem, m_beltSystem,
@@ -244,11 +244,13 @@ void Simulation::placeInitialStructures()
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));
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
WeaponComponent psWeapon; WeaponComponent psWeapon;
psWeapon.damage = static_cast<float>( psWeapon.damage = static_cast<float>(
m_config.stations.playerStation.damageFormula.evaluate(psLevel)); m_config.stations.playerStation.damageFormula.evaluate(psLevel));
psWeapon.range = static_cast<float>( psWeapon.range_tiles = static_cast<float>(
m_config.stations.playerStation.rangeFormula.evaluate(psLevel)); m_config.stations.playerStation.rangeFormula.evaluate(psLevel)) / tileSize;
psWeapon.fireRateHz = static_cast<float>( psWeapon.fireRateHz = static_cast<float>(
m_config.stations.playerStation.fireRateFormula.evaluate(psLevel)); m_config.stations.playerStation.fireRateFormula.evaluate(psLevel));
psWeapon.cooldownTicks = 0.0f; psWeapon.cooldownTicks = 0.0f;
@@ -303,6 +305,7 @@ void Simulation::placeInitialStructures()
void Simulation::placeEnemyStationSet(int generation) void Simulation::placeEnemyStationSet(int generation)
{ {
const float tileSize = static_cast<float>(m_config.world.tileSize_m);
const ParsedSurfaceMask esParsed = const ParsedSurfaceMask esParsed =
parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East); parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East);
@@ -318,8 +321,8 @@ void Simulation::placeEnemyStationSet(int generation)
WeaponComponent esWeapon; WeaponComponent esWeapon;
esWeapon.damage = static_cast<float>( esWeapon.damage = static_cast<float>(
m_config.stations.enemyStation.damageFormula.evaluate(genD)); m_config.stations.enemyStation.damageFormula.evaluate(genD));
esWeapon.range = static_cast<float>( esWeapon.range_tiles = static_cast<float>(
m_config.stations.enemyStation.rangeFormula.evaluate(genD)); m_config.stations.enemyStation.rangeFormula.evaluate(genD)) / tileSize;
esWeapon.fireRateHz = static_cast<float>( esWeapon.fireRateHz = static_cast<float>(
m_config.stations.enemyStation.fireRateFormula.evaluate(genD)); m_config.stations.enemyStation.fireRateFormula.evaluate(genD));
esWeapon.cooldownTicks = 0.0f; esWeapon.cooldownTicks = 0.0f;

View File

@@ -62,7 +62,7 @@ struct Fixture
explicit Fixture() explicit Fixture()
: cfg(loadConfig()) : cfg(loadConfig())
, belts(cfg.world.beltSpeedTilesPerSecond) , belts(cfg.world.beltSpeed_tps)
, nextBuildingId(1) , nextBuildingId(1)
, stock(0) , stock(0)
, rng(42) , rng(42)
@@ -188,13 +188,13 @@ TEST_CASE("BehaviorSystem: clearMovementIntents resets all ships to priority 0",
// tickMovement // tickMovement
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeedPerTick toward target", TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeed_tpt toward target",
"[behavior]") "[behavior]")
{ {
Fixture f; Fixture f;
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick; const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{1, QVector2D(100.0f, 0.0f)}; f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{1, QVector2D(100.0f, 0.0f)};
f.movementIntent.tick(f.admin); f.movementIntent.tick(f.admin);
f.dynamicBody.tick(f.admin); f.dynamicBody.tick(f.admin);
@@ -209,7 +209,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
Fixture f; Fixture f;
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeedPerTick; const float speed = f.admin.get<DynamicBodyComponent>(e).maxSpeed_tpt;
const QVector2D target(speed * 0.5f, 0.0f); const QVector2D target(speed * 0.5f, 0.0f);
f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{1, target}; f.admin.get<MovementIntentComponent>(e) = MovementIntentComponent{1, target};
f.movementIntent.tick(f.admin); f.movementIntent.tick(f.admin);
@@ -610,7 +610,7 @@ TEST_CASE("BehaviorSystem: tickRepairTools does not crash when owner lacks Repai
const entt::entity moduleEntity = f.admin.createModuleEntity(); const entt::entity moduleEntity = f.admin.createModuleEntity();
RepairToolComponent rt; RepairToolComponent rt;
rt.ratePerTick = 1.0f; rt.ratePerTick = 1.0f;
rt.range = 10.0f; rt.range_tiles = 10.0f;
rt.currentTarget = std::nullopt; rt.currentTarget = std::nullopt;
f.admin.addComponent<RepairToolComponent>(moduleEntity, rt); f.admin.addComponent<RepairToolComponent>(moduleEntity, rt);
f.admin.addComponent<ModuleOwnerComponent>(moduleEntity, ModuleOwnerComponent{ownerShip}); f.admin.addComponent<ModuleOwnerComponent>(moduleEntity, ModuleOwnerComponent{ownerShip});
@@ -861,7 +861,7 @@ TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn",
{ {
Fixture f; Fixture f;
const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
REQUIRE(f.admin.get<SensorRangeComponent>(e).value == Approx(200.0f)); REQUIRE(f.admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -71,7 +71,7 @@ static void runTicks(BuildingSystem& bs, BeltSystem& belts, int n, Tick& tick)
TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building]") TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -96,7 +96,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -124,7 +124,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]") TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -144,7 +144,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]") TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -177,7 +177,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -194,7 +194,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]") TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -215,7 +215,7 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]") TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -239,7 +239,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
TEST_CASE("BuildingSystem: second building starts after first completes", "[building]") TEST_CASE("BuildingSystem: second building starts after first completes", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -268,7 +268,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[building]") TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -297,7 +297,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -456,7 +456,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -544,7 +544,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -562,7 +562,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -584,7 +584,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -610,7 +610,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -631,7 +631,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -654,7 +654,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -682,7 +682,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -704,7 +704,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -727,7 +727,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;
@@ -757,7 +757,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
BuildingId nextBuildingId = 1; BuildingId nextBuildingId = 1;

View File

@@ -66,7 +66,7 @@ struct CombatFixture
: cfg(loadConfig()) : cfg(loadConfig())
, rng(42) , rng(42)
, nextBuildingId(1) , nextBuildingId(1)
, belts(cfg.world.beltSpeedTilesPerSecond) , belts(cfg.world.beltSpeed_tps)
, ships(cfg, admin) , ships(cfg, admin)
, buildings(cfg, belts, , buildings(cfg, belts,
[this]() { return nextBuildingId++; }, [this]() { return nextBuildingId++; },

View File

@@ -70,7 +70,7 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
// world.toml // world.toml
REQUIRE(cfg.world.heightTiles == 60); REQUIRE(cfg.world.heightTiles == 60);
REQUIRE(cfg.world.refundPercentage == 75); REQUIRE(cfg.world.refundPercentage == 75);
REQUIRE(cfg.world.beltSpeedTilesPerSecond == Approx(2.0)); REQUIRE(cfg.world.beltSpeed_tps == Approx(2.0));
REQUIRE(cfg.world.regions.asteroidWidth == 40); REQUIRE(cfg.world.regions.asteroidWidth == 40);
REQUIRE(cfg.world.regions.playerBufferWidth == 10); REQUIRE(cfg.world.regions.playerBufferWidth == 10);
REQUIRE(cfg.world.regions.enemyBufferWidth == 15); REQUIRE(cfg.world.regions.enemyBufferWidth == 15);
@@ -158,7 +158,8 @@ TEST_CASE("Missing field in world.toml is rejected with the field path", "[confi
height_tiles = 60 height_tiles = 60
refund_percentage = 75 refund_percentage = 75
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 tile_size_m = 10
belt_speed_mps = 20
starting_building_blocks = 100 starting_building_blocks = 100
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20 departure_interval_seconds = 20
@@ -205,7 +206,8 @@ TEST_CASE("Malformed formula in world.toml is rejected with field identification
height_tiles = 60 height_tiles = 60
refund_percentage = 75 refund_percentage = 75
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 tile_size_m = 10
belt_speed_mps = 20
starting_building_blocks = 100 starting_building_blocks = 100
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20 departure_interval_seconds = 20
@@ -253,7 +255,8 @@ TEST_CASE("Inverted wave gap range is rejected", "[config]")
height_tiles = 60 height_tiles = 60
refund_percentage = 75 refund_percentage = 75
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 tile_size_m = 10
belt_speed_mps = 20
[regions] [regions]
asteroid_width = 40 asteroid_width = 40

View File

@@ -41,7 +41,7 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
REQUIRE(sensor.statModifiers.size() == 1); REQUIRE(sensor.statModifiers.size() == 1);
CHECK(sensor.statModifiers[0].stat == "sensor_range"); CHECK(sensor.statModifiers[0].stat == "sensor_range");
CHECK(sensor.statModifiers[0].modifierType == "additive"); CHECK(sensor.statModifiers[0].modifierType == "additive");
CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(10.0)); CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(100.0));
} }
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]") TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")

View File

@@ -139,7 +139,8 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const double x = static_cast<double>(def->schematic.playerProductionLevel); const double x = static_cast<double>(def->schematic.playerProductionLevel);
const float baseRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x)); const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
const float baseRange_tiles = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x)) / tileSize;
ShipLayoutConfig layout; ShipLayoutConfig layout;
PlacedModule pm; PlacedModule pm;
@@ -153,9 +154,9 @@ 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 = "10" // sensor_booster has added_sensor_range_formula = "100" m → 100/10 = 10 tiles
// final = base * 1.0 + 10 = base + 10 // final = baseRange_tiles * 1.0 + 10 = baseRange_tiles + 10
CHECK(sim.admin().get<SensorRangeComponent>(e).value == Approx(baseRange + 10.0f)); CHECK(sim.admin().get<SensorRangeComponent>(e).value_tiles == Approx(baseRange_tiles + 10.0f));
} }
TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]") TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")

View File

@@ -110,14 +110,14 @@ 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 = "200" // sensor_range_formula = "2000" m → 2000/10 = 200 tiles
REQUIRE(admin.get<SensorRangeComponent>(e).value == Approx(200.0f)); REQUIRE(admin.get<SensorRangeComponent>(e).value_tiles == Approx(200.0f));
// laser_cannon: damage_formula = "2", attack_range_formula = "5" // laser_cannon: damage_formula = "2", attack_range_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));
REQUIRE(admin.get<WeaponComponent>(wc).range == Approx(5.0f)); REQUIRE(admin.get<WeaponComponent>(wc).range_tiles == Approx(5.0f));
REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f)); REQUIRE(admin.get<WeaponComponent>(wc).cooldownTicks == Approx(0.0f));
} }
@@ -133,7 +133,7 @@ TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(65.0f)); REQUIRE(admin.get<HealthComponent>(e).maxHp == Approx(65.0f));
} }
TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTickRateHz", "[ship]") TEST_CASE("ShipSystem: interceptor level 0 maxSpeed_tpt matches formula / tileSize / kTickRateHz", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadConfig();
@@ -141,9 +141,9 @@ TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTi
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 = "200 + 5*x" at x=0 → 200; maxSpeedPerTick = 200/30 // speed_formula = "2000 + 50*x" m/s at x=0 → 2000 m/s; maxSpeed_tpt = 2000/(10*30)
const float expected = 200.0f / static_cast<float>(kTickRateHz); const float expected = 2000.0f / 10.0f / static_cast<float>(kTickRateHz);
REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeedPerTick == Approx(expected)); REQUIRE(admin.get<DynamicBodyComponent>(e).maxSpeed_tpt == Approx(expected));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -175,14 +175,14 @@ 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 = "50" // salvager: cargo_capacity_formula = "10", collection_range_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);
REQUIRE(admin.get<SalvageCargoComponent>(sc).current == 0); REQUIRE(admin.get<SalvageCargoComponent>(sc).current == 0);
REQUIRE(admin.get<SalvageBehaviorComponent>(e).deliveryBay == kInvalidBuildingId); REQUIRE(admin.get<SalvageBehaviorComponent>(e).deliveryBay == kInvalidBuildingId);
REQUIRE_FALSE(admin.get<SalvageBehaviorComponent>(e).scrapTarget.has_value()); REQUIRE_FALSE(admin.get<SalvageBehaviorComponent>(e).scrapTarget.has_value());
REQUIRE(admin.get<SalvageBehaviorComponent>(e).maxCollectionRange == Approx(50.0f)); REQUIRE(admin.get<SalvageBehaviorComponent>(e).maxCollectionRange_tiles == Approx(50.0f));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -219,9 +219,9 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
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 = "80" // repair_range_formula = "800" m → 800/10 = 80 tiles
REQUIRE(admin.get<RepairToolComponent>(rc).range == Approx(80.0f)); REQUIRE(admin.get<RepairToolComponent>(rc).range_tiles == Approx(80.0f));
REQUIRE(admin.get<RepairBehaviorComponent>(e).maxRepairRange == Approx(80.0f)); REQUIRE(admin.get<RepairBehaviorComponent>(e).maxRepairRange_tiles == Approx(80.0f));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -139,7 +139,7 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
{ {
++armedPlayerStations; ++armedPlayerStations;
REQUIRE(w.damage > 0.0f); REQUIRE(w.damage > 0.0f);
REQUIRE(w.range > 0.0f); REQUIRE(w.range_tiles > 0.0f);
REQUIRE(w.fireRateHz > 0.0f); REQUIRE(w.fireRateHz > 0.0f);
} }
}); });
@@ -160,7 +160,7 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
{ {
++armedEnemyStations; ++armedEnemyStations;
REQUIRE(w.damage > 0.0f); REQUIRE(w.damage > 0.0f);
REQUIRE(w.range > 0.0f); REQUIRE(w.range_tiles > 0.0f);
REQUIRE(w.fireRateHz > 0.0f); REQUIRE(w.fireRateHz > 0.0f);
} }
}); });

View File

@@ -930,7 +930,7 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
if (it == m_visuals->ships.end()) { return; } if (it == m_visuals->ships.end()) { return; }
const QPointF center = worldToWidget(pos.value); const QPointF center = worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value) const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(tilePx()); * static_cast<qreal>(tilePx());
painter.setPen(QPen(it->second.outline, 1)); painter.setPen(QPen(it->second.outline, 1));
painter.drawEllipse(center, radiusPx, radiusPx); painter.drawEllipse(center, radiusPx, radiusPx);