implement sensor range requirements
This commit is contained in:
@@ -16,6 +16,9 @@ hp_formula = "4"
|
||||
[ship.movement]
|
||||
speed_formula = "4"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "15"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "1"
|
||||
attack_range_formula = "10"
|
||||
@@ -43,6 +46,9 @@ hp_formula = "120 + 15*x"
|
||||
[ship.movement]
|
||||
speed_formula = "120"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "300"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "12 + 2*x"
|
||||
attack_range_formula = "250"
|
||||
@@ -70,6 +76,9 @@ hp_formula = "40 + 4*x"
|
||||
[ship.movement]
|
||||
speed_formula = "110"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.salvage]
|
||||
collection_range = 50
|
||||
cargo_capacity = 10
|
||||
@@ -96,6 +105,9 @@ hp_formula = "60 + 5*x"
|
||||
[ship.movement]
|
||||
speed_formula = "130"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.repair]
|
||||
repair_rate_formula = "5 + x"
|
||||
repair_range_formula = "80"
|
||||
|
||||
@@ -397,6 +397,14 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
|
||||
}
|
||||
|
||||
// Sensor
|
||||
{
|
||||
const std::string snsPath = elemPath + ".sensor";
|
||||
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
|
||||
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
||||
def.sensor.sensorRangeFormula = requireFormula(snsMt["sensor_range_formula"], file, snsPath + ".sensor_range_formula");
|
||||
}
|
||||
|
||||
// Loot
|
||||
{
|
||||
const std::string lPath = elemPath + ".loot";
|
||||
|
||||
@@ -33,6 +33,11 @@ struct ShipMovement
|
||||
Formula speedFormula; // REQ-SHP-STATS, REQ-SHP-MOVEMENT
|
||||
};
|
||||
|
||||
struct ShipSensor
|
||||
{
|
||||
Formula sensorRangeFormula; // REQ-SHP-SENSOR, REQ-SHP-STATS
|
||||
};
|
||||
|
||||
struct ShipCombat
|
||||
{
|
||||
Formula damageFormula;
|
||||
@@ -69,6 +74,7 @@ struct ShipDef
|
||||
ShipThreat threat;
|
||||
ShipHealth health;
|
||||
ShipMovement movement;
|
||||
ShipSensor sensor;
|
||||
ShipLoot loot;
|
||||
|
||||
// Role-specific sections. A ship is a combat ship if combat is present,
|
||||
|
||||
@@ -41,7 +41,6 @@ struct RepairTool
|
||||
|
||||
struct ThreatResponse
|
||||
{
|
||||
float engagementRange;
|
||||
std::optional<EntityId> currentTarget;
|
||||
};
|
||||
|
||||
@@ -79,6 +78,7 @@ struct Ship
|
||||
float hp;
|
||||
float maxHp;
|
||||
float speedPerTick; // pre-evaluated from speedFormula / kTickRateHz
|
||||
float sensorRange; // pre-evaluated from sensorRangeFormula (REQ-SHP-SENSOR)
|
||||
int level;
|
||||
std::string schematicId;
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
|
||||
ship.speedPerTick = static_cast<float>(
|
||||
def->movement.speedFormula.evaluate(x))
|
||||
/ static_cast<float>(kTickRateHz);
|
||||
ship.sensorRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x));
|
||||
ship.level = level;
|
||||
ship.schematicId = schematicId;
|
||||
ship.isEnemy = isEnemy;
|
||||
@@ -61,9 +62,7 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
|
||||
w.cooldownTicks = 0.0f;
|
||||
ship.weapon = w;
|
||||
|
||||
ThreatResponse tr;
|
||||
tr.engagementRange = w.range;
|
||||
ship.threatResponse = tr;
|
||||
ship.threatResponse = ThreatResponse{};
|
||||
|
||||
if (!isEnemy)
|
||||
{
|
||||
@@ -241,7 +240,7 @@ void ShipSystem::tickThreatResponse(const BuildingSystem& buildings)
|
||||
continue;
|
||||
}
|
||||
|
||||
const float range = s.threatResponse->engagementRange;
|
||||
const float range = s.sensorRange;
|
||||
|
||||
if (!s.isEnemy)
|
||||
{
|
||||
@@ -394,7 +393,6 @@ void ShipSystem::tickThreatResponse(const BuildingSystem& buildings)
|
||||
void ShipSystem::tickRepairBehavior(BuildingSystem& buildings)
|
||||
{
|
||||
const std::vector<Building> allBuildings = buildings.allBuildings();
|
||||
const float kPatrolRange = 3.0f; // scan radius relative to repair range
|
||||
|
||||
for (Ship& s : m_ships)
|
||||
{
|
||||
@@ -410,7 +408,7 @@ void ShipSystem::tickRepairBehavior(BuildingSystem& buildings)
|
||||
for (const Ship& candidate : m_ships)
|
||||
{
|
||||
if (candidate.isEnemy
|
||||
&& (candidate.position - s.position).length() <= repairRange)
|
||||
&& (candidate.position - s.position).length() <= s.sensorRange)
|
||||
{
|
||||
enemyNearby = true;
|
||||
break;
|
||||
@@ -450,7 +448,7 @@ void ShipSystem::tickRepairBehavior(BuildingSystem& buildings)
|
||||
{
|
||||
s.repairBehavior->currentTarget = std::nullopt;
|
||||
currentId = kInvalidEntityId;
|
||||
float bestDist = repairRange * kPatrolRange;
|
||||
float bestDist = s.sensorRange;
|
||||
|
||||
for (const Ship& candidate : m_ships)
|
||||
{
|
||||
@@ -640,8 +638,7 @@ void ShipSystem::tickScrapCollector(ScrapSystem& scraps, const BuildingSystem& b
|
||||
else
|
||||
{
|
||||
// Scan for nearest scrap within sensor range.
|
||||
const float sensorRange = collectRange * 5.0f;
|
||||
float bestDist = sensorRange;
|
||||
float bestDist = s.sensorRange;
|
||||
std::optional<QVector2D> bestPos;
|
||||
for (const Scrap& sc : scraps.allScraps())
|
||||
{
|
||||
|
||||
@@ -439,3 +439,132 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
||||
REQUIRE(s->intent.priority == 1);
|
||||
REQUIRE(s->intent.target.x() < s->position.x());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sensor range — spawn
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("SensorRange: sensorRange is populated from config formula at spawn", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// interceptor sensor_range_formula = "200" (test config); verify at level 1.
|
||||
const EntityId id = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
REQUIRE(f.ships.findShip(id)->sensorRange == Approx(200.0f));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sensor range — tickThreatResponse
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("SensorRange: player combat ship acquires enemy just inside sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// interceptor sensor_range = 200 (test config); enemy at 190 tiles.
|
||||
const EntityId playerId = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const EntityId enemyId = f.ships.spawn("interceptor", 1, QVector2D(190.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ships.tickThreatResponse(f.buildings);
|
||||
|
||||
const Ship* player = f.ships.findShip(playerId);
|
||||
REQUIRE(player->threatResponse->currentTarget == enemyId);
|
||||
}
|
||||
|
||||
TEST_CASE("SensorRange: player combat ship ignores enemy just outside sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// interceptor sensor_range = 200 (test config); enemy at 210 tiles.
|
||||
const EntityId playerId = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ships.tickThreatResponse(f.buildings);
|
||||
|
||||
const Ship* player = f.ships.findShip(playerId);
|
||||
REQUIRE_FALSE(player->threatResponse->currentTarget.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("SensorRange: enemy ship ignores player just outside sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// interceptor sensor_range = 200 (test config); player at 210 tiles from enemy.
|
||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const EntityId enemyId = f.ships.spawn("interceptor", 1, QVector2D(210.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ships.tickThreatResponse(f.buildings);
|
||||
|
||||
const Ship* enemy = f.ships.findShip(enemyId);
|
||||
REQUIRE_FALSE(enemy->threatResponse->currentTarget.has_value());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sensor range — tickRepairBehavior
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("SensorRange: repair ship retreats from enemy within sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// repair_ship sensor_range = 250; enemy at 200 tiles.
|
||||
const EntityId repairId = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", 1, QVector2D(200.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ships.tickRepairBehavior(f.buildings);
|
||||
|
||||
const Ship* repair = f.ships.findShip(repairId);
|
||||
REQUIRE(repair->intent.priority == 2);
|
||||
REQUIRE(repair->intent.target.x() < 0.0f); // retreating leftward
|
||||
}
|
||||
|
||||
TEST_CASE("SensorRange: repair ship does not retreat from enemy beyond sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// repair_ship sensor_range = 250; enemy at 300 tiles.
|
||||
const EntityId repairId = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f), /*isEnemy=*/true);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ships.tickRepairBehavior(f.buildings);
|
||||
|
||||
// Enemy outside sensor range → repair ship patrols rightward instead of retreating.
|
||||
const Ship* repair = f.ships.findShip(repairId);
|
||||
REQUIRE(repair->intent.target.x() > repair->position.x());
|
||||
}
|
||||
|
||||
TEST_CASE("SensorRange: repair ship does not acquire damaged ally beyond sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// repair_ship sensor_range = 250; damaged friendly at 300 tiles.
|
||||
const EntityId repairId = f.ships.spawn("repair_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
const EntityId friendlyId = f.ships.spawn("interceptor", 1, QVector2D(300.0f, 0.0f));
|
||||
f.ships.forEach([friendlyId](Ship& s) {
|
||||
if (s.id == friendlyId) { s.hp = s.maxHp * 0.5f; }
|
||||
});
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ships.tickRepairBehavior(f.buildings);
|
||||
|
||||
REQUIRE_FALSE(f.ships.findShip(repairId)->repairBehavior->currentTarget.has_value());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sensor range — tickScrapCollector
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("SensorRange: salvage ship ignores scrap beyond sensor range", "[sensor]")
|
||||
{
|
||||
Fixture f;
|
||||
// salvage_ship sensor_range = 250; scrap at 300 tiles.
|
||||
const EntityId shipId = f.ships.spawn("salvage_ship", 1, QVector2D(0.0f, 0.0f));
|
||||
f.scraps.spawn(QVector2D(300.0f, 0.0f), 1, 100000);
|
||||
|
||||
f.ships.clearMovementIntents();
|
||||
f.ships.tickScrapCollector(f.scraps, f.buildings);
|
||||
|
||||
const Ship* s = f.ships.findShip(shipId);
|
||||
REQUIRE(s->scrapCollector->scrapTarget == std::nullopt);
|
||||
REQUIRE(s->intent.target.x() > s->position.x()); // patrolling rightward
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
|
||||
REQUIRE(ship->weapon->damage == Approx(12.0f));
|
||||
// attack_range_formula = "150"
|
||||
REQUIRE(ship->weapon->range == Approx(150.0f));
|
||||
// threatResponse.engagementRange mirrors weapon range
|
||||
REQUIRE(ship->threatResponse->engagementRange == Approx(150.0f));
|
||||
// sensor_range_formula = "200"
|
||||
REQUIRE(ship->sensorRange == Approx(200.0f));
|
||||
// cooldownTicks starts at 0
|
||||
REQUIRE(ship->weapon->cooldownTicks == Approx(0.0f));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ hp_formula = "40 + 5*x"
|
||||
[ship.movement]
|
||||
speed_formula = "200 + 5*x"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "200"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "10 + 2*x"
|
||||
attack_range_formula = "150"
|
||||
@@ -43,6 +46,9 @@ hp_formula = "120 + 15*x"
|
||||
[ship.movement]
|
||||
speed_formula = "120"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "300"
|
||||
|
||||
[ship.combat]
|
||||
damage_formula = "12 + 2*x"
|
||||
attack_range_formula = "250"
|
||||
@@ -70,6 +76,9 @@ hp_formula = "40 + 4*x"
|
||||
[ship.movement]
|
||||
speed_formula = "110"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.salvage]
|
||||
collection_range = 50
|
||||
cargo_capacity = 10
|
||||
@@ -96,6 +105,9 @@ hp_formula = "60 + 5*x"
|
||||
[ship.movement]
|
||||
speed_formula = "130"
|
||||
|
||||
[ship.sensor]
|
||||
sensor_range_formula = "250"
|
||||
|
||||
[ship.repair]
|
||||
repair_rate_formula = "5 + x"
|
||||
repair_range_formula = "80"
|
||||
|
||||
@@ -804,23 +804,7 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter)
|
||||
m_visuals->ships.find(role);
|
||||
if (it == m_visuals->ships.end()) { continue; }
|
||||
|
||||
float range = 0.0f;
|
||||
if (ship.weapon.has_value())
|
||||
{
|
||||
range = ship.weapon->range;
|
||||
}
|
||||
else if (ship.repairTool.has_value())
|
||||
{
|
||||
range = ship.repairTool->range;
|
||||
}
|
||||
else if (ship.cargo.has_value())
|
||||
{
|
||||
range = ship.cargo->collectionRange * 5.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float range = ship.sensorRange;
|
||||
|
||||
const QPointF center = worldToWidget(ship.position);
|
||||
const qreal radiusPx = static_cast<qreal>(range)
|
||||
|
||||
Reference in New Issue
Block a user