diff --git a/bin/app/data/config/modules.toml b/bin/app/data/config/modules.toml index 2e884f5..8b1ffe1 100644 --- a/bin/app/data/config/modules.toml +++ b/bin/app/data/config/modules.toml @@ -10,7 +10,7 @@ glyph = "L" [module.weapon] damage_formula = "2" -attack_range_formula = "5" +attack_range_formula = "50" attack_rate_formula = "2.0" [[module]] @@ -24,7 +24,7 @@ fill_color = "#AACC44" glyph = "Sv" [module.salvage] -collection_range_formula = "50" +collection_range_formula = "500" cargo_capacity_formula = "10" collection_rate_formula = "0.5" @@ -40,4 +40,4 @@ glyph = "Rp" [module.repair] repair_rate_formula = "5 + x" -repair_range_formula = "80" +repair_range_formula = "800" diff --git a/bin/app/data/config/ships.toml b/bin/app/data/config/ships.toml index 6f5b41a..c84891c 100644 --- a/bin/app/data/config/ships.toml +++ b/bin/app/data/config/ships.toml @@ -16,14 +16,14 @@ cost_formula = "10" hp_formula = "3" [ship.movement] -speed_formula = "4" -main_acceleration_formula = "8" -maneuvering_acceleration_formula = "4" +speed_formula = "40" +main_acceleration_formula = "80" +maneuvering_acceleration_formula = "40" angular_acceleration_formula = "12.56" max_rotation_speed_formula = "6.28" [ship.sensor] -sensor_range_formula = "15" +sensor_range_formula = "150" [ship.loot] scrap_drop = 2 diff --git a/bin/app/data/config/stations.toml b/bin/app/data/config/stations.toml index 26edb51..cbde215 100644 --- a/bin/app/data/config/stations.toml +++ b/bin/app/data/config/stations.toml @@ -14,7 +14,7 @@ surface_mask = [ level = 1 hp_formula = "300" damage_formula = "5" -range_formula = "20" +range_formula = "200" fire_rate_formula = "1" scrap_drop_formula = "10" @@ -25,6 +25,6 @@ surface_mask = [ ] hp_formula = "300 + 150*x" damage_formula = "2 + 1*x" -range_formula = "20" +range_formula = "200" fire_rate_formula = "1.0 + 0.2*x" scrap_drop_formula = "10 + 5*x" diff --git a/bin/app/data/config/world.toml b/bin/app/data/config/world.toml index 0dd0add..623b49f 100644 --- a/bin/app/data/config/world.toml +++ b/bin/app/data/config/world.toml @@ -3,7 +3,8 @@ height_tiles = 30 refund_percentage = 75 starting_building_blocks = 1000 scrap_despawn_seconds = 30 -belt_speed_tiles_per_second = 2 +tile_size_m = 10 +belt_speed_mps = 20 tunnel_max_distance = 10 departure_interval_seconds = 20 diff --git a/bin/test/data/config/modules.toml b/bin/test/data/config/modules.toml index cd6f594..b686296 100644 --- a/bin/test/data/config/modules.toml +++ b/bin/test/data/config/modules.toml @@ -22,7 +22,7 @@ fill_color = "#40A0FF" glyph = "S" [module.sensor] -added_sensor_range_formula = "10" +added_sensor_range_formula = "100" [[module]] id = "weapon_upgrade" @@ -49,7 +49,7 @@ glyph = "L" [module.weapon] damage_formula = "2" -attack_range_formula = "5" +attack_range_formula = "50" attack_rate_formula = "2.0" [[module]] @@ -63,7 +63,7 @@ fill_color = "#AACC44" glyph = "Sv" [module.salvage] -collection_range_formula = "50" +collection_range_formula = "500" cargo_capacity_formula = "10" collection_rate_formula = "0.5" @@ -79,4 +79,4 @@ glyph = "Rp" [module.repair] repair_rate_formula = "5 + x" -repair_range_formula = "80" +repair_range_formula = "800" diff --git a/bin/test/data/config/ships.toml b/bin/test/data/config/ships.toml index 080c8ad..ad78f78 100644 --- a/bin/test/data/config/ships.toml +++ b/bin/test/data/config/ships.toml @@ -16,14 +16,14 @@ cost_formula = "5 + 1*x" hp_formula = "40 + 5*x" [ship.movement] -speed_formula = "200 + 5*x" -main_acceleration_formula = "100000" -maneuvering_acceleration_formula = "100000" +speed_formula = "2000 + 50*x" +main_acceleration_formula = "1000000" +maneuvering_acceleration_formula = "1000000" angular_acceleration_formula = "100000" max_rotation_speed_formula = "100000" [ship.sensor] -sensor_range_formula = "200" +sensor_range_formula = "2000" [ship.loot] scrap_drop = 2 @@ -47,14 +47,14 @@ cost_formula = "10 + 2*x" hp_formula = "120 + 15*x" [ship.movement] -speed_formula = "120" -main_acceleration_formula = "100000" -maneuvering_acceleration_formula = "100000" +speed_formula = "1200" +main_acceleration_formula = "1000000" +maneuvering_acceleration_formula = "1000000" angular_acceleration_formula = "100000" max_rotation_speed_formula = "100000" [ship.sensor] -sensor_range_formula = "300" +sensor_range_formula = "3000" [ship.loot] scrap_drop = 4 @@ -77,14 +77,14 @@ cost_formula = "0" hp_formula = "40 + 4*x" [ship.movement] -speed_formula = "110" -main_acceleration_formula = "100000" -maneuvering_acceleration_formula = "100000" +speed_formula = "1100" +main_acceleration_formula = "1000000" +maneuvering_acceleration_formula = "1000000" angular_acceleration_formula = "100000" max_rotation_speed_formula = "100000" [ship.sensor] -sensor_range_formula = "250" +sensor_range_formula = "2500" [ship.loot] scrap_drop = 2 @@ -107,14 +107,14 @@ cost_formula = "0" hp_formula = "60 + 5*x" [ship.movement] -speed_formula = "130" -main_acceleration_formula = "100000" -maneuvering_acceleration_formula = "100000" +speed_formula = "1300" +main_acceleration_formula = "1000000" +maneuvering_acceleration_formula = "1000000" angular_acceleration_formula = "100000" max_rotation_speed_formula = "100000" [ship.sensor] -sensor_range_formula = "250" +sensor_range_formula = "2500" [ship.loot] scrap_drop = 2 diff --git a/bin/test/data/config/stations.toml b/bin/test/data/config/stations.toml index 926d848..c7558af 100644 --- a/bin/test/data/config/stations.toml +++ b/bin/test/data/config/stations.toml @@ -14,7 +14,7 @@ surface_mask = [ level = 5 hp_formula = "300 + 40*x" damage_formula = "5 + 4*x" -range_formula = "300 + 20*x" +range_formula = "3000 + 200*x" fire_rate_formula = "0.5 + 0.2*x" scrap_drop_formula = "x" @@ -25,6 +25,6 @@ surface_mask = [ ] hp_formula = "300 + 150*x" damage_formula = "20 + 10*x" -range_formula = "350 + 20*x" +range_formula = "3500 + 200*x" fire_rate_formula = "1.0 + 0.2*x" scrap_drop_formula = "10 + 5*x" diff --git a/bin/test/data/config/world.toml b/bin/test/data/config/world.toml index a3fda86..f754191 100644 --- a/bin/test/data/config/world.toml +++ b/bin/test/data/config/world.toml @@ -3,7 +3,8 @@ height_tiles = 60 refund_percentage = 75 starting_building_blocks = 100 scrap_despawn_seconds = 30 -belt_speed_tiles_per_second = 2 +tile_size_m = 10 +belt_speed_mps = 20 tunnel_max_distance = 10 departure_interval_seconds = 20 diff --git a/src/balancing/ArenaSimulation.cpp b/src/balancing/ArenaSimulation.cpp index 4927d3c..423b476 100644 --- a/src/balancing/ArenaSimulation.cpp +++ b/src/balancing/ArenaSimulation.cpp @@ -123,6 +123,7 @@ void ArenaSimulation::placeStructures() weapon.cooldownTicks = 0.0f; weapon.currentTarget = std::nullopt; const double lv = static_cast(entry.level); + const float tileSize = static_cast(m_gameConfig.world.tileSize_m); const std::vector& mask = isEnemy ? m_gameConfig.stations.enemyStation.surfaceMask @@ -134,8 +135,8 @@ void ArenaSimulation::placeStructures() m_gameConfig.stations.playerStation.hpFormula.evaluate(lv)); weapon.damage = static_cast( m_gameConfig.stations.playerStation.damageFormula.evaluate(lv)); - weapon.range = static_cast( - m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)); + weapon.range_tiles = static_cast( + m_gameConfig.stations.playerStation.rangeFormula.evaluate(lv)) / tileSize; weapon.fireRateHz = static_cast( m_gameConfig.stations.playerStation.fireRateFormula.evaluate(lv)); } @@ -145,8 +146,8 @@ void ArenaSimulation::placeStructures() m_gameConfig.stations.enemyStation.hpFormula.evaluate(lv)); weapon.damage = static_cast( m_gameConfig.stations.enemyStation.damageFormula.evaluate(lv)); - weapon.range = static_cast( - m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)); + weapon.range_tiles = static_cast( + m_gameConfig.stations.enemyStation.rangeFormula.evaluate(lv)) / tileSize; weapon.fireRateHz = static_cast( m_gameConfig.stations.enemyStation.fireRateFormula.evaluate(lv)); } diff --git a/src/lib/config/ConfigLoader.cpp b/src/lib/config/ConfigLoader.cpp index fa91e1c..452ce1d 100644 --- a/src/lib/config/ConfigLoader.cpp +++ b/src/lib/config/ConfigLoader.cpp @@ -264,7 +264,8 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path) cfg.refundPercentage = static_cast(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage")); cfg.startingBuildingBlocks = static_cast(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.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(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance")); cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds"); diff --git a/src/lib/config/WorldConfig.h b/src/lib/config/WorldConfig.h index 84181ab..c8d7192 100644 --- a/src/lib/config/WorldConfig.h +++ b/src/lib/config/WorldConfig.h @@ -45,7 +45,8 @@ struct WorldConfig int refundPercentage; // REQ-BLD-DEMOLISH int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS 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 double departureIntervalSeconds; // REQ-SHP-RALLY diff --git a/src/lib/core/EntityAdmin.cpp b/src/lib/core/EntityAdmin.cpp index 699af94..1d46fd9 100644 --- a/src/lib/core/EntityAdmin.cpp +++ b/src/lib/core/EntityAdmin.cpp @@ -39,9 +39,9 @@ void EntityAdmin::clear() } entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp, - float maxSpeedPerTick, float mainAccelPerTick, - float maneuveringAccelPerTick, float angularAccelPerTick, - float maxRotationSpeedPerTick, float sensorRange, + float maxSpeed_tpt, float mainAcceleration_tptt, + float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt, + float maxRotationSpeed_rpt, float sensorRange_tiles, int level, const std::string& schematicId, bool isEnemy) { entt::entity entity = createEntity(); @@ -50,17 +50,17 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp, add(entity, FactionComponent{isEnemy}); add(entity, FacingComponent{0.0f}); add(entity, DynamicBodyComponent{ - maxSpeedPerTick, - mainAccelPerTick, - maneuveringAccelPerTick, - angularAccelPerTick, - maxRotationSpeedPerTick, - QVector2D(0.0f, 0.0f), // velocity - 0.0f, // angularVelocity - QVector2D(0.0f, 0.0f), // linearAcceleration - 0.0f // angularAcceleration + maxSpeed_tpt, + mainAcceleration_tptt, + maneuveringAcceleration_tptt, + maxAngularAcceleration_rptt, + maxRotationSpeed_rpt, + QVector2D(0.0f, 0.0f), // velocity_tpt + 0.0f, // angularVelocity_rpt + QVector2D(0.0f, 0.0f), // linearAcceleration_tptt + 0.0f // angularAcceleration_rptt }); - add(entity, SensorRangeComponent{sensorRange}); + add(entity, SensorRangeComponent{sensorRange_tiles}); add(entity, ShipIdentityComponent{level, schematicId}); add(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)}); return entity; diff --git a/src/lib/core/EntityAdmin.h b/src/lib/core/EntityAdmin.h index 71d32aa..0ed1371 100644 --- a/src/lib/core/EntityAdmin.h +++ b/src/lib/core/EntityAdmin.h @@ -53,9 +53,9 @@ public: // -- Factory methods ---------------------------------------------------- entt::entity spawnShip(QVector2D position, float hp, float maxHp, - float maxSpeedPerTick, float mainAccelPerTick, - float maneuveringAccelPerTick, float angularAccelPerTick, - float maxRotationSpeedPerTick, float sensorRange, + float maxSpeed_tpt, float mainAcceleration_tptt, + float maneuveringAcceleration_tptt, float maxAngularAcceleration_rptt, + float maxRotationSpeed_rpt, float sensorRange_tiles, int level, const std::string& schematicId, bool isEnemy); entt::entity spawnStation(QPoint anchor, QSize footprint, diff --git a/src/lib/ecs/component/DynamicBodyComponent.h b/src/lib/ecs/component/DynamicBodyComponent.h index be28df9..d3cef3d 100644 --- a/src/lib/ecs/component/DynamicBodyComponent.h +++ b/src/lib/ecs/component/DynamicBodyComponent.h @@ -4,18 +4,18 @@ struct DynamicBodyComponent { - // --- dynamics parameters (formerly ShipDynamics) --- - float maxSpeedPerTick; - float mainAccelerationPerTick; - float maneuveringAccelerationPerTick; - float angularAccelerationPerTick; - float maxRotationSpeedPerTick; + // --- dynamics parameters --- + float maxSpeed_tpt; // tiles/tick + float mainAcceleration_tptt; // tiles/tick² + float maneuveringAcceleration_tptt; // tiles/tick² + float maxAngularAcceleration_rptt; // rad/tick² + float maxRotationSpeed_rpt; // rad/tick // --- integrated state --- - QVector2D velocity; - float angularVelocity; + QVector2D velocity_tpt; // tiles/tick + float angularVelocity_rpt; // rad/tick // --- written each tick by MovementIntentSystem, consumed by DynamicBodySystem --- - QVector2D linearAcceleration; - float angularAcceleration; + QVector2D linearAcceleration_tptt; // tiles/tick² + float angularAcceleration_rptt; // rad/tick² }; diff --git a/src/lib/ecs/component/RepairBehaviorComponent.h b/src/lib/ecs/component/RepairBehaviorComponent.h index a672c08..3841ef7 100644 --- a/src/lib/ecs/component/RepairBehaviorComponent.h +++ b/src/lib/ecs/component/RepairBehaviorComponent.h @@ -7,5 +7,5 @@ struct RepairBehaviorComponent { std::optional currentTarget; - float maxRepairRange = 0.0f; + float maxRepairRange_tiles = 0.0f; }; diff --git a/src/lib/ecs/component/RepairToolComponent.h b/src/lib/ecs/component/RepairToolComponent.h index 7a4f13d..14b4d8b 100644 --- a/src/lib/ecs/component/RepairToolComponent.h +++ b/src/lib/ecs/component/RepairToolComponent.h @@ -7,6 +7,6 @@ struct RepairToolComponent { float ratePerTick; - float range; + float range_tiles; std::optional currentTarget; }; diff --git a/src/lib/ecs/component/SalvageBehaviorComponent.h b/src/lib/ecs/component/SalvageBehaviorComponent.h index 32f486e..3864105 100644 --- a/src/lib/ecs/component/SalvageBehaviorComponent.h +++ b/src/lib/ecs/component/SalvageBehaviorComponent.h @@ -10,5 +10,5 @@ struct SalvageBehaviorComponent { std::optional scrapTarget; BuildingId deliveryBay; // kInvalidBuildingId until assigned at a salvage bay - float maxCollectionRange = 0.0f; + float maxCollectionRange_tiles = 0.0f; }; diff --git a/src/lib/ecs/component/SalvageCargoComponent.h b/src/lib/ecs/component/SalvageCargoComponent.h index 443a04f..014785e 100644 --- a/src/lib/ecs/component/SalvageCargoComponent.h +++ b/src/lib/ecs/component/SalvageCargoComponent.h @@ -4,7 +4,7 @@ struct SalvageCargoComponent { int capacity; int current; - float collectionRange; + float collectionRange_tiles; int collectionIntervalTicks; int cooldownTicksRemaining; }; diff --git a/src/lib/ecs/component/SensorRangeComponent.h b/src/lib/ecs/component/SensorRangeComponent.h index 4aa75b8..b2f71c5 100644 --- a/src/lib/ecs/component/SensorRangeComponent.h +++ b/src/lib/ecs/component/SensorRangeComponent.h @@ -2,5 +2,5 @@ struct SensorRangeComponent { - float value; + float value_tiles; }; diff --git a/src/lib/ecs/component/WeaponComponent.h b/src/lib/ecs/component/WeaponComponent.h index 14b945e..3a9ee51 100644 --- a/src/lib/ecs/component/WeaponComponent.h +++ b/src/lib/ecs/component/WeaponComponent.h @@ -7,7 +7,7 @@ struct WeaponComponent { float damage; - float range; + float range_tiles; float fireRateHz; float cooldownTicks; std::optional currentTarget; diff --git a/src/lib/ecs/system/AiSystem.cpp b/src/lib/ecs/system/AiSystem.cpp index e9940f6..f46620a 100644 --- a/src/lib/ecs/system/AiSystem.cpp +++ b/src/lib/ecs/system/AiSystem.cpp @@ -132,7 +132,7 @@ void AiSystem::tickThreatResponseBehavior(EntityAdmin& admin, const BuildingSyst PositionComponent& pos, FactionComponent& faction, SensorRangeComponent& sensor, MovementIntentComponent& intent) { - const float range = sensor.value; + const float range = sensor.value_tiles; // Validate current target. bool targetValid = false; @@ -251,7 +251,7 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings) bool enemyNearby = false; for (const EnemyInfo& enemy : enemies) { - if ((enemy.position - pos.value).length() <= sensor.value) + if ((enemy.position - pos.value).length() <= sensor.value_tiles) { enemyNearby = true; break; @@ -285,7 +285,7 @@ void AiSystem::tickRepairBehavior(EntityAdmin& admin, BuildingSystem& buildings) if (!targetValid) { rb.currentTarget = std::nullopt; - float bestDist = sensor.value; + float bestDist = sensor.value_tiles; for (const RepairableInfo& r : repairables) { @@ -355,7 +355,7 @@ void AiSystem::tickRepairTools(EntityAdmin& admin) const float dist = (admin.get(preferred).value - 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; 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. rt.currentTarget = std::nullopt; - float bestDist = rt.range; + float bestDist = rt.range_tiles; for (const RepairableInfo& r : repairables) { if (r.isEnemy) { continue; } @@ -441,7 +441,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, PositionComponent& pos, SensorRangeComponent& sensor, MovementIntentComponent& intent) { - const float collectRange = salvageBehavior.maxCollectionRange; + const float collectRange = salvageBehavior.maxCollectionRange_tiles; const AggregatedCargo& cargoState = cargoByShip[e]; // Assign nearest SalvageBay if needed. @@ -530,7 +530,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, } 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)) { ++c.current; @@ -555,7 +555,7 @@ void AiSystem::tickSalvageBehavior(EntityAdmin& admin, ScrapSystem& scraps, } else { - float bestDist = sensor.value; + float bestDist = sensor.value_tiles; std::optional bestPos; for (const ScrapInfo& si : allScrap) { diff --git a/src/lib/ecs/system/CombatSystem.cpp b/src/lib/ecs/system/CombatSystem.cpp index 884d86a..42202ee 100644 --- a/src/lib/ecs/system/CombatSystem.cpp +++ b/src/lib/ecs/system/CombatSystem.cpp @@ -69,7 +69,7 @@ void CombatSystem::resolveWeapon( { const float distanceSquared = (ownPos.value - admin.get(t).value).lengthSquared(); - if (distanceSquared > weapon.range * weapon.range) + if (distanceSquared > weapon.range_tiles * weapon.range_tiles) { weapon.currentTarget = std::nullopt; } @@ -81,8 +81,8 @@ void CombatSystem::resolveWeapon( if (!weapon.currentTarget) { const float acquisitionRange = admin.hasAll(shipEntity) - ? admin.get(shipEntity).value - : weapon.range; + ? admin.get(shipEntity).value_tiles + : weapon.range_tiles; float bestDistanceSquared = acquisitionRange * acquisitionRange; admin.forEach( [&](entt::entity candidate, const ShipIdentityComponent& /*si*/, diff --git a/src/lib/ecs/system/DynamicBodySystem.cpp b/src/lib/ecs/system/DynamicBodySystem.cpp index 0aab334..b03fb6c 100644 --- a/src/lib/ecs/system/DynamicBodySystem.cpp +++ b/src/lib/ecs/system/DynamicBodySystem.cpp @@ -28,27 +28,27 @@ void DynamicBodySystem::tick(EntityAdmin& admin) DynamicBodyComponent& body) { // Integrate angular velocity, clamp to max rotation speed, then advance facing. - body.angularVelocity += body.angularAcceleration; - body.angularVelocity = std::max(-body.maxRotationSpeedPerTick, - std::min(body.angularVelocity, - body.maxRotationSpeedPerTick)); - facing.radians = wrapAngle(facing.radians + body.angularVelocity); + body.angularVelocity_rpt += body.angularAcceleration_rptt; + body.angularVelocity_rpt = std::max(-body.maxRotationSpeed_rpt, + std::min(body.angularVelocity_rpt, + body.maxRotationSpeed_rpt)); + facing.radians = wrapAngle(facing.radians + body.angularVelocity_rpt); // Integrate linear velocity and cap to max speed. - body.velocity += body.linearAcceleration; - const float speed = body.velocity.length(); - if (speed > body.maxSpeedPerTick) + body.velocity_tpt += body.linearAcceleration_tptt; + const float speed = body.velocity_tpt.length(); + if (speed > body.maxSpeed_tpt) { - body.velocity = body.velocity.normalized() * body.maxSpeedPerTick; + body.velocity_tpt = body.velocity_tpt.normalized() * body.maxSpeed_tpt; } // Advance position. - pos.value += body.velocity; + pos.value += body.velocity_tpt; // Reset per-tick fields so stale values don't linger if the intent // system is skipped for this entity in a future tick. - body.linearAcceleration = QVector2D(0.0f, 0.0f); - body.angularAcceleration = 0.0f; + body.linearAcceleration_tptt = QVector2D(0.0f, 0.0f); + body.angularAcceleration_rptt = 0.0f; }); } diff --git a/src/lib/ecs/system/MovementIntentSystem.cpp b/src/lib/ecs/system/MovementIntentSystem.cpp index 8acf04e..48a9626 100644 --- a/src/lib/ecs/system/MovementIntentSystem.cpp +++ b/src/lib/ecs/system/MovementIntentSystem.cpp @@ -32,16 +32,16 @@ void MovementIntentSystem::tick(EntityAdmin& admin) if (intent.priority == 0) { // No movement intent: brake using available thrust. - const float linearBraking = std::min(body.velocity.length(), - body.maneuveringAccelerationPerTick); - body.linearAcceleration = (body.velocity.length() > 0.0001f) - ? -body.velocity.normalized() * linearBraking + const float linearBraking = std::min(body.velocity_tpt.length(), + body.maneuveringAcceleration_tptt); + body.linearAcceleration_tptt = (body.velocity_tpt.length() > 0.0001f) + ? -body.velocity_tpt.normalized() * linearBraking : QVector2D(0.0f, 0.0f); - const float angBraking = std::min(std::abs(body.angularVelocity), - body.angularAccelerationPerTick); - body.angularAcceleration = - (body.angularVelocity >= 0.0f) ? -angBraking : angBraking; + const float angBraking = std::min(std::abs(body.angularVelocity_rpt), + body.maxAngularAcceleration_rptt); + body.angularAcceleration_rptt = + (body.angularVelocity_rpt >= 0.0f) ? -angBraking : angBraking; return; } @@ -52,8 +52,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin) { // Already at target: no new thrust. The ship drifts; it will // re-approach next tick once it has moved away. - body.linearAcceleration = QVector2D(0.0f, 0.0f); - body.angularAcceleration = 0.0f; + body.linearAcceleration_tptt = QVector2D(0.0f, 0.0f); + body.angularAcceleration_rptt = 0.0f; return; } @@ -62,11 +62,11 @@ void MovementIntentSystem::tick(EntityAdmin& admin) const float desiredAngle = std::atan2(delta.y(), delta.x()); 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, - body.angularAccelerationPerTick)); + body.maxAngularAcceleration_rptt)); - float newAngVel = body.angularVelocity + rotDelta; + float newAngVel = body.angularVelocity_rpt + rotDelta; // Overshoot prevention: if the accumulated angular velocity already // exceeds the remaining angle, snap it to exactly that angle so the @@ -77,8 +77,8 @@ void MovementIntentSystem::tick(EntityAdmin& admin) newAngVel = angleDiff; } - body.angularAcceleration = newAngVel - body.angularVelocity; - // DynamicBodySystem applies the clamp to maxRotationSpeedPerTick after + body.angularAcceleration_rptt = newAngVel - body.angularVelocity_rpt; + // DynamicBodySystem applies the clamp to maxRotationSpeed_rpt after // integrating, so we do not clamp here. // --- Linear acceleration --- @@ -90,22 +90,22 @@ void MovementIntentSystem::tick(EntityAdmin& admin) const QVector2D facingVec(std::cos(projectedRadians), std::sin(projectedRadians)); - const float manAccel = body.maneuveringAccelerationPerTick; - const float stoppingDist = (body.maxSpeedPerTick * body.maxSpeedPerTick) + const float manAccel = body.maneuveringAcceleration_tptt; + const float stoppingDist = (body.maxSpeed_tpt * body.maxSpeed_tpt) / (2.0f * manAccel); // Cap to dist so the ship never overshoots the target in a single tick. const float baseDesiredSpeed = (dist <= stoppingDist) ? std::sqrt(2.0f * manAccel * dist) - : body.maxSpeedPerTick; + : body.maxSpeed_tpt; const float desiredSpeed = std::min(dist, baseDesiredSpeed); 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, QVector2D::dotProduct(velError, facingVec)); const float mainApplied = std::min(mainAligned, - body.mainAccelerationPerTick); + body.mainAcceleration_tptt); const QVector2D mainDelta = facingVec * mainApplied; const QVector2D remaining = velError - mainDelta; @@ -114,7 +114,7 @@ void MovementIntentSystem::tick(EntityAdmin& admin) ? remaining.normalized() * manAccel : remaining; - body.linearAcceleration = mainDelta + maneuverDelta; + body.linearAcceleration_tptt = mainDelta + maneuverDelta; }); } diff --git a/src/lib/ecs/system/ShipSystem.cpp b/src/lib/ecs/system/ShipSystem.cpp index 701c434..d9572bc 100644 --- a/src/lib/ecs/system/ShipSystem.cpp +++ b/src/lib/ecs/system/ShipSystem.cpp @@ -62,30 +62,32 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, const double x = static_cast(level); const float tickRate = static_cast(kTickRateHz); + const float tileSize = static_cast(m_config.world.tileSize_m); float hp = static_cast(def->health.hpFormula.evaluate(x)); float maxHp = hp; - float maxSpeedPerTick = static_cast(def->movement.speedFormula.evaluate(x)) - / tickRate; - float mainAccelPerTick = static_cast( - def->movement.mainAccelerationFormula.evaluate(x)) - / tickRate; - float maneuveringAccelPerTick = static_cast( - def->movement.maneuveringAccelerationFormula.evaluate(x)) - / tickRate; - float angularAccelPerTick = static_cast( - def->movement.angularAccelerationFormula.evaluate(x)) - / tickRate; - float maxRotationSpeedPerTick = static_cast( - def->movement.maxRotationSpeedFormula.evaluate(x)) - / tickRate; - float sensorRange = static_cast( - def->sensor.sensorRangeFormula.evaluate(x)); + float maxSpeed_tpt = static_cast(def->movement.speedFormula.evaluate(x)) + / tileSize / tickRate; + float mainAcceleration_tptt = static_cast( + def->movement.mainAccelerationFormula.evaluate(x)) + / tileSize / tickRate; + float maneuveringAcceleration_tptt = static_cast( + def->movement.maneuveringAccelerationFormula.evaluate(x)) + / tileSize / tickRate; + float maxAngularAcceleration_rptt = static_cast( + def->movement.angularAccelerationFormula.evaluate(x)) + / tickRate; + float maxRotationSpeed_rpt = static_cast( + def->movement.maxRotationSpeedFormula.evaluate(x)) + / tickRate; + float sensorRange_tiles = static_cast( + def->sensor.sensorRangeFormula.evaluate(x)) + / tileSize; entt::entity entity = m_admin.spawnShip( position, hp, maxHp, - maxSpeedPerTick, mainAccelPerTick, maneuveringAccelPerTick, - angularAccelPerTick, maxRotationSpeedPerTick, sensorRange, + maxSpeed_tpt, mainAcceleration_tptt, maneuveringAcceleration_tptt, + maxAngularAcceleration_rptt, maxRotationSpeed_rpt, sensorRange_tiles, level, schematicId, isEnemy); // 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; w.damage = static_cast( modDef->weaponCapability->damageFormula.evaluate(mx)); - w.range = static_cast( - modDef->weaponCapability->attackRangeFormula.evaluate(mx)); + w.range_tiles = static_cast( + modDef->weaponCapability->attackRangeFormula.evaluate(mx)) / tileSize; w.fireRateHz = static_cast( modDef->weaponCapability->attackRateFormula.evaluate(mx)); w.cooldownTicks = 0.0f; @@ -128,8 +130,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, cargo.capacity = static_cast( modDef->salvageCapability->cargoCapacityFormula.evaluate(mx)); cargo.current = 0; - cargo.collectionRange = static_cast( - modDef->salvageCapability->collectionRangeFormula.evaluate(mx)); + cargo.collectionRange_tiles = static_cast( + modDef->salvageCapability->collectionRangeFormula.evaluate(mx)) / tileSize; const double rate = modDef->salvageCapability->collectionRateFormula.evaluate(mx); cargo.collectionIntervalTicks = (rate > 0.0) ? static_cast(kTickRateHz / rate + 0.5) @@ -148,8 +150,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, rt.ratePerTick = static_cast( modDef->repairCapability->repairRateFormula.evaluate(mx)) / static_cast(kTickRateHz); - rt.range = static_cast( - modDef->repairCapability->repairRangeFormula.evaluate(mx)); + rt.range_tiles = static_cast( + modDef->repairCapability->repairRangeFormula.evaluate(mx)) / tileSize; rt.currentTarget = std::nullopt; 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(m_config.world.tileSize_m); + const char* const kRangeStats[] = { + "sensor_range", "attack_range", "collection_range", "repair_range" + }; + std::map>* allModMaps[] = { + &hullMods, &weaponMods, &salvageMods, &repairMods + }; + for (const char* stat : kRangeStats) + { + for (std::map>* mods : allModMaps) + { + std::map>::iterator it = mods->find(stat); + if (it != mods->end()) + { + it->second.second /= tileSizeD; + } + } + } + // Helper: apply a modifier map to a float stat. auto applyMod = [](float& stat, const std::string& name, const std::map>& mods) @@ -226,30 +248,30 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, DynamicBodyComponent& dynamics = m_admin.get(entity); SensorRangeComponent& sensor = m_admin.get(entity); - applyMod(health.maxHp, "hp", hullMods); + applyMod(health.maxHp, "hp", hullMods); health.hp = health.maxHp; - applyMod(dynamics.maxSpeedPerTick, "speed", hullMods); - applyMod(dynamics.mainAccelerationPerTick, "main_acceleration", hullMods); - applyMod(dynamics.maneuveringAccelerationPerTick, "maneuvering_acceleration", hullMods); - applyMod(dynamics.angularAccelerationPerTick, "angular_acceleration", hullMods); - applyMod(dynamics.maxRotationSpeedPerTick, "max_rotation_speed", hullMods); - applyMod(sensor.value, "sensor_range", hullMods); + applyMod(dynamics.maxSpeed_tpt, "speed", hullMods); + applyMod(dynamics.mainAcceleration_tptt, "main_acceleration", hullMods); + applyMod(dynamics.maneuveringAcceleration_tptt, "maneuvering_acceleration", hullMods); + applyMod(dynamics.maxAngularAcceleration_rptt, "angular_acceleration", hullMods); + applyMod(dynamics.maxRotationSpeed_rpt, "max_rotation_speed", hullMods); + applyMod(sensor.value_tiles, "sensor_range", hullMods); } // Apply weapon modifiers to each weapon child. for (entt::entity child : weaponChildren) { WeaponComponent& w = m_admin.get(child); - applyMod(w.damage, "damage", weaponMods); - applyMod(w.range, "attack_range", weaponMods); - applyMod(w.fireRateHz, "attack_rate", weaponMods); + applyMod(w.damage, "damage", weaponMods); + applyMod(w.range_tiles, "attack_range", weaponMods); + applyMod(w.fireRateHz, "attack_rate", weaponMods); } // Apply salvage modifiers to each salvage child. for (entt::entity child : salvageChildren) { SalvageCargoComponent& c = m_admin.get(child); - float fRange = c.collectionRange; + float fRange = c.collectionRange_tiles; float fCapacity = static_cast(c.capacity); // Apply rate modifier: compute rate from interval, apply multiplier, convert back. 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(fCapacity, "cargo_capacity", salvageMods); applyMod(fRate, "collection_rate", salvageMods); - c.collectionRange = fRange; + c.collectionRange_tiles = fRange; c.capacity = static_cast(fCapacity + 0.5f); c.collectionIntervalTicks = (fRate > 0.0f) ? static_cast(static_cast(kTickRateHz) / fRate + 0.5f) @@ -269,8 +291,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, for (entt::entity child : repairChildren) { RepairToolComponent& rt = m_admin.get(child); - applyMod(rt.ratePerTick, "repair_rate", repairMods); - applyMod(rt.range, "repair_range", repairMods); + applyMod(rt.ratePerTick, "repair_rate", repairMods); + applyMod(rt.range_tiles, "repair_range", repairMods); } // --- 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; for (entt::entity child : salvageChildren) { - const float r = m_admin.get(child).collectionRange; + const float r = m_admin.get(child).collectionRange_tiles; if (r > maxCollRange) { maxCollRange = r; } } SalvageBehaviorComponent sb; - sb.scrapTarget = std::nullopt; - sb.deliveryBay = kInvalidBuildingId; - sb.maxCollectionRange = maxCollRange; + sb.scrapTarget = std::nullopt; + sb.deliveryBay = kInvalidBuildingId; + sb.maxCollectionRange_tiles = maxCollRange; m_admin.addComponent(entity, sb); } @@ -308,13 +330,13 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level, float maxRepairRange = 0.0f; for (entt::entity child : repairChildren) { - const float r = m_admin.get(child).range; + const float r = m_admin.get(child).range_tiles; if (r > maxRepairRange) { maxRepairRange = r; } } RepairBehaviorComponent rb; - rb.currentTarget = std::nullopt; - rb.maxRepairRange = maxRepairRange; + rb.currentTarget = std::nullopt; + rb.maxRepairRange_tiles = maxRepairRange; m_admin.addComponent(entity, rb); } diff --git a/src/lib/sim/BeltSystem.cpp b/src/lib/sim/BeltSystem.cpp index 7d847cb..9f3b6b0 100644 --- a/src/lib/sim/BeltSystem.cpp +++ b/src/lib/sim/BeltSystem.cpp @@ -47,8 +47,8 @@ QPointF BeltSystem::slotWorldPos(QPoint tile, Rotation dir, double progress) // Construction / placement // --------------------------------------------------------------------------- -BeltSystem::BeltSystem(double beltSpeedTilesPerSecond) - : m_progressPerTick(beltSpeedTilesPerSecond * kTickDurationSeconds) +BeltSystem::BeltSystem(double beltSpeed_tps) + : m_progressPerTick_tpt(beltSpeed_tps * kTickDurationSeconds) { } @@ -414,7 +414,7 @@ void BeltSystem::advanceProgress() 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. 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) { - st.back[i].progress += m_progressPerTick; + st.back[i].progress += m_progressPerTick_tpt; const double absoluteCap = 0.5 - i * 0.25; if (st.back[i].progress > absoluteCap) { @@ -465,7 +465,7 @@ void BeltSystem::advanceProgress() if (st.frontA) { - st.frontA->progress += m_progressPerTick; + st.frontA->progress += m_progressPerTick_tpt; if (st.frontA->progress > 1.0) { st.frontA->progress = 1.0; @@ -474,7 +474,7 @@ void BeltSystem::advanceProgress() if (st.frontB) { - st.frontB->progress += m_progressPerTick; + st.frontB->progress += m_progressPerTick_tpt; if (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) { - te.itemSlots[i].progress += m_progressPerTick; + te.itemSlots[i].progress += m_progressPerTick_tpt; const double absoluteCap = 1.0 - i * 0.25; if (te.itemSlots[i].progress > absoluteCap) @@ -518,7 +518,7 @@ void BeltSystem::advanceTunnelProgress() 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; if (tx.itemSlots[i].progress > absoluteCap) @@ -542,7 +542,7 @@ void BeltSystem::advanceTunnelProgress() for (std::size_t i = 0; i < link.items.size(); ++i) { TunnelTransitItem& ti = link.items[i]; - ti.progress += m_progressPerTick; + ti.progress += m_progressPerTick_tpt; if (ti.progress > link.length) { ti.progress = link.length; diff --git a/src/lib/sim/BeltSystem.h b/src/lib/sim/BeltSystem.h index 48feda3..7006fe0 100644 --- a/src/lib/sim/BeltSystem.h +++ b/src/lib/sim/BeltSystem.h @@ -31,7 +31,7 @@ struct VisualItem class BeltSystem { public: - explicit BeltSystem(double beltSpeedTilesPerSecond); + explicit BeltSystem(double beltSpeed_tps); // -- Placement ----------------------------------------------------------- // Register a new belt tile. Any items already on this tile are cleared. @@ -170,7 +170,7 @@ private: std::vector items; // front (highest progress) to back }; - double m_progressPerTick; // beltSpeedTilesPerSecond / kTickRateHz + double m_progressPerTick_tpt; // beltSpeed_tps / kTickRateHz std::map, BeltTile> m_belts; std::map, SplitterTile> m_splitters; diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index 8de047b..09ce63d 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -33,7 +33,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed) , m_hqProxyEntity(entt::null) , m_playerStation1Entity(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[1] = entt::null; @@ -110,7 +110,7 @@ void Simulation::reset(unsigned int seed) m_schematicDropEvents.clear(); m_admin.clear(); - m_beltSystem = BeltSystem(m_config.world.beltSpeedTilesPerSecond); + m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps); m_buildingSystem = std::make_unique( m_config, m_beltSystem, @@ -244,11 +244,13 @@ void Simulation::placeInitialStructures() const float psHp = static_cast( m_config.stations.playerStation.hpFormula.evaluate(psLevel)); + const float tileSize = static_cast(m_config.world.tileSize_m); + WeaponComponent psWeapon; psWeapon.damage = static_cast( m_config.stations.playerStation.damageFormula.evaluate(psLevel)); - psWeapon.range = static_cast( - m_config.stations.playerStation.rangeFormula.evaluate(psLevel)); + psWeapon.range_tiles = static_cast( + m_config.stations.playerStation.rangeFormula.evaluate(psLevel)) / tileSize; psWeapon.fireRateHz = static_cast( m_config.stations.playerStation.fireRateFormula.evaluate(psLevel)); psWeapon.cooldownTicks = 0.0f; @@ -303,6 +305,7 @@ void Simulation::placeInitialStructures() void Simulation::placeEnemyStationSet(int generation) { + const float tileSize = static_cast(m_config.world.tileSize_m); const ParsedSurfaceMask esParsed = parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East); @@ -318,8 +321,8 @@ void Simulation::placeEnemyStationSet(int generation) WeaponComponent esWeapon; esWeapon.damage = static_cast( m_config.stations.enemyStation.damageFormula.evaluate(genD)); - esWeapon.range = static_cast( - m_config.stations.enemyStation.rangeFormula.evaluate(genD)); + esWeapon.range_tiles = static_cast( + m_config.stations.enemyStation.rangeFormula.evaluate(genD)) / tileSize; esWeapon.fireRateHz = static_cast( m_config.stations.enemyStation.fireRateFormula.evaluate(genD)); esWeapon.cooldownTicks = 0.0f; diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index 4570ebe..b32945b 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -62,7 +62,7 @@ struct Fixture explicit Fixture() : cfg(loadConfig()) - , belts(cfg.world.beltSpeedTilesPerSecond) + , belts(cfg.world.beltSpeed_tps) , nextBuildingId(1) , stock(0) , rng(42) @@ -188,13 +188,13 @@ TEST_CASE("BehaviorSystem: clearMovementIntents resets all ships to priority 0", // tickMovement // --------------------------------------------------------------------------- -TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeedPerTick toward target", +TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeed_tpt toward target", "[behavior]") { Fixture f; const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); - const float speed = f.admin.get(e).maxSpeedPerTick; + const float speed = f.admin.get(e).maxSpeed_tpt; f.admin.get(e) = MovementIntentComponent{1, QVector2D(100.0f, 0.0f)}; f.movementIntent.tick(f.admin); f.dynamicBody.tick(f.admin); @@ -209,7 +209,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo Fixture f; const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); - const float speed = f.admin.get(e).maxSpeedPerTick; + const float speed = f.admin.get(e).maxSpeed_tpt; const QVector2D target(speed * 0.5f, 0.0f); f.admin.get(e) = MovementIntentComponent{1, target}; 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(); RepairToolComponent rt; rt.ratePerTick = 1.0f; - rt.range = 10.0f; + rt.range_tiles = 10.0f; rt.currentTarget = std::nullopt; f.admin.addComponent(moduleEntity, rt); f.admin.addComponent(moduleEntity, ModuleOwnerComponent{ownerShip}); @@ -861,7 +861,7 @@ TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn", { Fixture f; const entt::entity e = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f)); - REQUIRE(f.admin.get(e).value == Approx(200.0f)); + REQUIRE(f.admin.get(e).value_tiles == Approx(200.0f)); } // --------------------------------------------------------------------------- diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index 247189e..f995f74 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -96,7 +96,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con "[building]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); 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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); 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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -177,7 +177,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately "[building]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); 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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); 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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); 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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); 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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); 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]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -456,7 +456,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max "[building]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -544,7 +544,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -562,7 +562,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -584,7 +584,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -610,7 +610,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -631,7 +631,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -654,7 +654,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -682,7 +682,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -704,7 +704,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -727,7 +727,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; @@ -757,7 +757,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste "[building][rotate-in-place]") { const GameConfig cfg = loadConfig(); - BeltSystem belts(cfg.world.beltSpeedTilesPerSecond); + BeltSystem belts(cfg.world.beltSpeed_tps); int stock = 0; std::mt19937 rng(0); BuildingId nextBuildingId = 1; diff --git a/src/test/CombatSystemTest.cpp b/src/test/CombatSystemTest.cpp index 879a022..3dc3877 100644 --- a/src/test/CombatSystemTest.cpp +++ b/src/test/CombatSystemTest.cpp @@ -66,7 +66,7 @@ struct CombatFixture : cfg(loadConfig()) , rng(42) , nextBuildingId(1) - , belts(cfg.world.beltSpeedTilesPerSecond) + , belts(cfg.world.beltSpeed_tps) , ships(cfg, admin) , buildings(cfg, belts, [this]() { return nextBuildingId++; }, diff --git a/src/test/ConfigLoaderTest.cpp b/src/test/ConfigLoaderTest.cpp index 7c0824f..5829ce7 100644 --- a/src/test/ConfigLoaderTest.cpp +++ b/src/test/ConfigLoaderTest.cpp @@ -70,7 +70,7 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c // world.toml REQUIRE(cfg.world.heightTiles == 60); 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.playerBufferWidth == 10); 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 refund_percentage = 75 scrap_despawn_seconds = 30 -belt_speed_tiles_per_second = 2 +tile_size_m = 10 +belt_speed_mps = 20 starting_building_blocks = 100 tunnel_max_distance = 10 departure_interval_seconds = 20 @@ -205,7 +206,8 @@ TEST_CASE("Malformed formula in world.toml is rejected with field identification height_tiles = 60 refund_percentage = 75 scrap_despawn_seconds = 30 -belt_speed_tiles_per_second = 2 +tile_size_m = 10 +belt_speed_mps = 20 starting_building_blocks = 100 tunnel_max_distance = 10 departure_interval_seconds = 20 @@ -253,7 +255,8 @@ TEST_CASE("Inverted wave gap range is rejected", "[config]") height_tiles = 60 refund_percentage = 75 scrap_despawn_seconds = 30 -belt_speed_tiles_per_second = 2 +tile_size_m = 10 +belt_speed_mps = 20 [regions] asteroid_width = 40 diff --git a/src/test/ModuleConfigTest.cpp b/src/test/ModuleConfigTest.cpp index ca07faa..ab7490e 100644 --- a/src/test/ModuleConfigTest.cpp +++ b/src/test/ModuleConfigTest.cpp @@ -41,7 +41,7 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul REQUIRE(sensor.statModifiers.size() == 1); CHECK(sensor.statModifiers[0].stat == "sensor_range"); CHECK(sensor.statModifiers[0].modifierType == "additive"); - CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(10.0)); + CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(100.0)); } TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]") diff --git a/src/test/ShipModuleTest.cpp b/src/test/ShipModuleTest.cpp index 973bc82..1668856 100644 --- a/src/test/ShipModuleTest.cpp +++ b/src/test/ShipModuleTest.cpp @@ -139,7 +139,8 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]") REQUIRE(def != nullptr); const double x = static_cast(def->schematic.playerProductionLevel); - const float baseRange = static_cast(def->sensor.sensorRangeFormula.evaluate(x)); + const float tileSize = static_cast(sim.config().world.tileSize_m); + const float baseRange_tiles = static_cast(def->sensor.sensorRangeFormula.evaluate(x)) / tileSize; ShipLayoutConfig layout; PlacedModule pm; @@ -153,9 +154,9 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]") QVector2D(5.0f, 5.0f), false, layout); REQUIRE(sim.admin().isValid(e)); - // sensor_booster has added_sensor_range_formula = "10" - // final = base * 1.0 + 10 = base + 10 - CHECK(sim.admin().get(e).value == Approx(baseRange + 10.0f)); + // sensor_booster has added_sensor_range_formula = "100" m → 100/10 = 10 tiles + // final = baseRange_tiles * 1.0 + 10 = baseRange_tiles + 10 + CHECK(sim.admin().get(e).value_tiles == Approx(baseRange_tiles + 10.0f)); } TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]") diff --git a/src/test/ShipTest.cpp b/src/test/ShipTest.cpp index 2c08da1..4b78180 100644 --- a/src/test/ShipTest.cpp +++ b/src/test/ShipTest.cpp @@ -110,14 +110,14 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship] // hp_formula = "40 + 5*x" at x=1 → 45 REQUIRE(admin.get(e).maxHp == Approx(45.0f)); REQUIRE(admin.get(e).hp == Approx(45.0f)); - // sensor_range_formula = "200" - REQUIRE(admin.get(e).value == Approx(200.0f)); + // sensor_range_formula = "2000" m → 2000/10 = 200 tiles + REQUIRE(admin.get(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); REQUIRE(admin.isValid(wc)); - REQUIRE(admin.get(wc).damage == Approx(2.0f)); - REQUIRE(admin.get(wc).range == Approx(5.0f)); + REQUIRE(admin.get(wc).damage == Approx(2.0f)); + REQUIRE(admin.get(wc).range_tiles == Approx(5.0f)); REQUIRE(admin.get(wc).cooldownTicks == Approx(0.0f)); } @@ -133,7 +133,7 @@ TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]") REQUIRE(admin.get(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; 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)); - // speed_formula = "200 + 5*x" at x=0 → 200; maxSpeedPerTick = 200/30 - const float expected = 200.0f / static_cast(kTickRateHz); - REQUIRE(admin.get(e).maxSpeedPerTick == Approx(expected)); + // speed_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(kTickRateHz); + REQUIRE(admin.get(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 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); REQUIRE(admin.isValid(sc)); REQUIRE(admin.get(sc).capacity == 10); REQUIRE(admin.get(sc).current == 0); REQUIRE(admin.get(e).deliveryBay == kInvalidBuildingId); REQUIRE_FALSE(admin.get(e).scrapTarget.has_value()); - REQUIRE(admin.get(e).maxCollectionRange == Approx(50.0f)); + REQUIRE(admin.get(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); REQUIRE(admin.isValid(rc)); REQUIRE(admin.get(rc).ratePerTick == Approx(expectedRate)); - // repair_range_formula = "80" - REQUIRE(admin.get(rc).range == Approx(80.0f)); - REQUIRE(admin.get(e).maxRepairRange == Approx(80.0f)); + // repair_range_formula = "800" m → 800/10 = 80 tiles + REQUIRE(admin.get(rc).range_tiles == Approx(80.0f)); + REQUIRE(admin.get(e).maxRepairRange_tiles == Approx(80.0f)); } // --------------------------------------------------------------------------- diff --git a/src/test/WaveSystemTest.cpp b/src/test/WaveSystemTest.cpp index d360134..7673369 100644 --- a/src/test/WaveSystemTest.cpp +++ b/src/test/WaveSystemTest.cpp @@ -139,7 +139,7 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]") { ++armedPlayerStations; REQUIRE(w.damage > 0.0f); - REQUIRE(w.range > 0.0f); + REQUIRE(w.range_tiles > 0.0f); REQUIRE(w.fireRateHz > 0.0f); } }); @@ -160,7 +160,7 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]") { ++armedEnemyStations; REQUIRE(w.damage > 0.0f); - REQUIRE(w.range > 0.0f); + REQUIRE(w.range_tiles > 0.0f); REQUIRE(w.fireRateHz > 0.0f); } }); diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 2ae218b..ab0f388 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -930,7 +930,7 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter) if (it == m_visuals->ships.end()) { return; } const QPointF center = worldToWidget(pos.value); - const qreal radiusPx = static_cast(sensor.value) + const qreal radiusPx = static_cast(sensor.value_tiles) * static_cast(tilePx()); painter.setPen(QPen(it->second.outline, 1)); painter.drawEllipse(center, radiusPx, radiusPx);