implement waves
This commit is contained in:
@@ -12,4 +12,6 @@ add_files(
|
||||
ShipTest.cpp
|
||||
ScrapTest.cpp
|
||||
BehaviorSystemTest.cpp
|
||||
WaveSystemTest.cpp
|
||||
CombatSystemTest.cpp
|
||||
)
|
||||
|
||||
351
src/test/CombatSystemTest.cpp
Normal file
351
src/test/CombatSystemTest.cpp
Normal file
@@ -0,0 +1,351 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <random>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "CombatSystem.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "FireEvent.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "Ship.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||
}
|
||||
|
||||
// Find the first ShipDef with a combat component.
|
||||
static const ShipDef* findCombatShip(const GameConfig& cfg)
|
||||
{
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
if (def.combat)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ship weapon firing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||
EntityId nextShipId = 1;
|
||||
EntityId nextBldId = 100;
|
||||
ShipSystem ships(cfg, [&nextShipId]() { return nextShipId++; });
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
rng);
|
||||
|
||||
// Spawn an enemy combat ship close to the player side.
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1,
|
||||
QVector2D(5.0f, 5.0f), /*isEnemy=*/true);
|
||||
|
||||
// Spawn a player combat ship in front of the enemy.
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1,
|
||||
QVector2D(4.0f, 5.0f), /*isEnemy=*/false);
|
||||
|
||||
// Wire the enemy's weapon target manually.
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 0.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Record player HP before combat.
|
||||
float hpBefore = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpBefore = s.hp; }
|
||||
}
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
combat.tick(0, ships, buildings, events);
|
||||
|
||||
float hpAfter = 0.0f;
|
||||
for (const Ship& s : ships.allShips())
|
||||
{
|
||||
if (s.id == playerId) { hpAfter = s.hp; }
|
||||
}
|
||||
|
||||
REQUIRE(hpAfter < hpBefore);
|
||||
REQUIRE(events.size() >= 1);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||
EntityId nextShipId = 1;
|
||||
EntityId nextBldId = 100;
|
||||
ShipSystem ships(cfg, [&nextShipId]() { return nextShipId++; });
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||
|
||||
// Set cooldown to 3 so it won't fire on tick 0 or 1 or 2.
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 3.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
|
||||
// Ticks 0 and 1: cooldown still > 0 after decrement → no fire.
|
||||
combat.tick(0, ships, buildings, events);
|
||||
combat.tick(1, ships, buildings, events);
|
||||
|
||||
REQUIRE(events.empty());
|
||||
|
||||
// Tick 2: cooldown reaches 0 → fires.
|
||||
combat.tick(2, ships, buildings, events);
|
||||
REQUIRE(events.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
BeltSystem belts(cfg.world.beltSpeedTilesPerSecond);
|
||||
EntityId nextShipId = 1;
|
||||
EntityId nextBldId = 100;
|
||||
ShipSystem ships(cfg, [&nextShipId]() { return nextShipId++; });
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
||||
const EntityId playerId = ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false);
|
||||
|
||||
ships.forEach([&](Ship& s)
|
||||
{
|
||||
if (s.id == enemyId && s.weapon)
|
||||
{
|
||||
s.weapon->currentTarget = playerId;
|
||||
s.weapon->cooldownTicks = 0.0f;
|
||||
if (s.threatResponse)
|
||||
{
|
||||
s.threatResponse->currentTarget = playerId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CombatSystem combat(cfg);
|
||||
std::vector<FireEvent> events;
|
||||
combat.tick(0, ships, buildings, events);
|
||||
|
||||
REQUIRE(events.empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Station weapon firing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
// Find the player defence station.
|
||||
EntityId stationId = kInvalidEntityId;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::PlayerDefenceStation)
|
||||
{
|
||||
stationId = b.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(stationId != kInvalidEntityId);
|
||||
|
||||
// Place an enemy ship close to the player station.
|
||||
QVector2D stationCenter;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.id == stationId)
|
||||
{
|
||||
stationCenter = QVector2D(
|
||||
b.anchor.x() + b.footprint.width() / 2.0f,
|
||||
b.anchor.y() + b.footprint.height() / 2.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find a combat ship blueprint for the enemy.
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const EntityId enemyId = sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
QVector2D(stationCenter.x() + 1.0f, stationCenter.y()),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
// Tick to let station auto-acquire and fire.
|
||||
sim.tick();
|
||||
|
||||
// Check that a fire event was emitted with stationId as shooter.
|
||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
||||
bool stationFired = false;
|
||||
for (const FireEvent& e : events)
|
||||
{
|
||||
if (e.shooter == stationId) { stationFired = true; }
|
||||
}
|
||||
REQUIRE(stationFired);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
// Find the enemy defence station.
|
||||
EntityId stationId = kInvalidEntityId;
|
||||
QVector2D stationCenter;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
{
|
||||
stationId = b.id;
|
||||
stationCenter = QVector2D(
|
||||
b.anchor.x() + b.footprint.width() / 2.0f,
|
||||
b.anchor.y() + b.footprint.height() / 2.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(stationId != kInvalidEntityId);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
// Spawn a player ship right next to the enemy station.
|
||||
sim.ships().spawn(
|
||||
combatDef->id, 1,
|
||||
QVector2D(stationCenter.x() - 1.0f, stationCenter.y()),
|
||||
/*isEnemy=*/false);
|
||||
|
||||
sim.tick();
|
||||
|
||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
||||
bool stationFired = false;
|
||||
for (const FireEvent& e : events)
|
||||
{
|
||||
if (e.shooter == stationId) { stationFired = true; }
|
||||
}
|
||||
REQUIRE(stationFired);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deaths & loot (tick step 9)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
const ShipDef* combatDef = findCombatShip(cfg);
|
||||
REQUIRE(combatDef != nullptr);
|
||||
|
||||
const EntityId shipId = sim.ships().spawn(combatDef->id, 1,
|
||||
QVector2D(10.0f, 10.0f));
|
||||
|
||||
// Set hp to lethal.
|
||||
sim.ships().damageShip(shipId, 9999.0f);
|
||||
|
||||
sim.tick();
|
||||
|
||||
REQUIRE(sim.ships().findShip(shipId) == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
// Find a ship def that drops scrap.
|
||||
const ShipDef* droppingDef = nullptr;
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
if (def.loot.scrapDrop > 0)
|
||||
{
|
||||
droppingDef = &def;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(droppingDef != nullptr);
|
||||
|
||||
const EntityId shipId = sim.ships().spawn(droppingDef->id, 1,
|
||||
QVector2D(10.0f, 10.0f));
|
||||
sim.ships().damageShip(shipId, 9999.0f);
|
||||
|
||||
sim.tick();
|
||||
|
||||
// At least one scrap entity should now exist.
|
||||
REQUIRE(!sim.scraps().allScraps().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
sim.buildings().forEachBuilding([](Building& b)
|
||||
{
|
||||
if (b.type == BuildingType::Hq)
|
||||
{
|
||||
b.hp = -1.0f;
|
||||
}
|
||||
});
|
||||
|
||||
sim.tick();
|
||||
|
||||
REQUIRE(sim.isGameOver());
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include "ConfigLoader.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "TickDriver.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simulation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
|
||||
{
|
||||
const GameConfig config;
|
||||
const GameConfig config = loadConfig();
|
||||
const Simulation sim(config);
|
||||
|
||||
REQUIRE(sim.currentTick() == 0);
|
||||
@@ -19,7 +25,7 @@ TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
|
||||
|
||||
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
||||
{
|
||||
const GameConfig config;
|
||||
const GameConfig config = loadConfig();
|
||||
Simulation sim(config);
|
||||
|
||||
sim.tick();
|
||||
@@ -29,7 +35,7 @@ TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
||||
|
||||
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
||||
{
|
||||
const GameConfig config;
|
||||
const GameConfig config = loadConfig();
|
||||
Simulation sim(config);
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
@@ -42,7 +48,7 @@ TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
||||
|
||||
TEST_CASE("Simulation::drainFireEvents returns empty initially", "[simulation]")
|
||||
{
|
||||
const GameConfig config;
|
||||
const GameConfig config = loadConfig();
|
||||
Simulation sim(config);
|
||||
|
||||
REQUIRE(sim.drainFireEvents().empty());
|
||||
@@ -50,7 +56,7 @@ TEST_CASE("Simulation::drainFireEvents returns empty initially", "[simulation]")
|
||||
|
||||
TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]")
|
||||
{
|
||||
const GameConfig config;
|
||||
const GameConfig config = loadConfig();
|
||||
Simulation sim(config);
|
||||
|
||||
// First drain: empty.
|
||||
@@ -62,7 +68,7 @@ TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]")
|
||||
|
||||
TEST_CASE("Simulation::drainBlueprintDropEvents returns empty initially", "[simulation]")
|
||||
{
|
||||
const GameConfig config;
|
||||
const GameConfig config = loadConfig();
|
||||
Simulation sim(config);
|
||||
|
||||
REQUIRE(sim.drainBlueprintDropEvents().empty());
|
||||
|
||||
357
src/test/WaveSystemTest.cpp
Normal file
357
src/test/WaveSystemTest.cpp
Normal file
@@ -0,0 +1,357 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <random>
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "Rotation.h"
|
||||
#include "Ship.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "WaveSystem.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Threat accumulation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("WaveSystem: threat stays 0 for first 30 game-seconds", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
WaveSystem ws(cfg, rng);
|
||||
|
||||
// threat_rate_formula = "1*x - 30", which is <= 0 for x <= 30.
|
||||
const int ticks30s = static_cast<int>(secondsToTicks(30.0));
|
||||
for (int i = 0; i < ticks30s; ++i)
|
||||
{
|
||||
ws.tickThreatAccumulation(static_cast<Tick>(i));
|
||||
}
|
||||
|
||||
REQUIRE(ws.threatLevel() == Approx(0.0));
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: threat accumulates after 30 game-seconds", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
WaveSystem ws(cfg, rng);
|
||||
|
||||
// Run 31 seconds worth of ticks.
|
||||
const int ticks31s = static_cast<int>(secondsToTicks(31.0));
|
||||
for (int i = 0; i < ticks31s; ++i)
|
||||
{
|
||||
ws.tickThreatAccumulation(static_cast<Tick>(i));
|
||||
}
|
||||
|
||||
REQUIRE(ws.threatLevel() > 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: applyPush increases threat accumulation rate", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
WaveSystem ws(cfg, rng);
|
||||
|
||||
// Accumulate for 1 tick past the 30s mark to get a baseline rate.
|
||||
const Tick baseTick = secondsToTicks(31.0);
|
||||
ws.tickThreatAccumulation(baseTick);
|
||||
const double levelBefore = ws.threatLevel();
|
||||
|
||||
// Apply push: multiplier should increase.
|
||||
ws.applyPush();
|
||||
|
||||
WaveSystem ws2(cfg, rng);
|
||||
ws2.tickThreatAccumulation(baseTick);
|
||||
|
||||
// After the push the same tick adds more threat.
|
||||
ws.tickThreatAccumulation(baseTick + 1);
|
||||
ws2.tickThreatAccumulation(baseTick + 1);
|
||||
|
||||
// ws has the push multiplier applied; ws2 does not.
|
||||
REQUIRE(ws.threatLevel() > ws2.threatLevel());
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: generation starts at 0 and increments on push", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
std::mt19937 rng(42);
|
||||
WaveSystem ws(cfg, rng);
|
||||
|
||||
REQUIRE(ws.generation() == 0);
|
||||
ws.applyPush();
|
||||
REQUIRE(ws.generation() == 1);
|
||||
ws.applyPush();
|
||||
REQUIRE(ws.generation() == 2);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pre-placed structures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const Simulation sim(cfg, 42);
|
||||
|
||||
int hqCount = 0;
|
||||
int playerCount = 0;
|
||||
int enemyCount = 0;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::Hq) { ++hqCount; }
|
||||
else if (b.type == BuildingType::PlayerDefenceStation) { ++playerCount; }
|
||||
else if (b.type == BuildingType::EnemyDefenceStation) { ++enemyCount; }
|
||||
}
|
||||
|
||||
REQUIRE(hqCount == 1);
|
||||
REQUIRE(playerCount == 2);
|
||||
REQUIRE(enemyCount == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const Simulation sim(cfg, 42);
|
||||
|
||||
const float expectedHp =
|
||||
static_cast<float>(cfg.stations.hq.hpFormula.evaluate(0.0));
|
||||
bool found = false;
|
||||
float actualHp = 0.0f;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::Hq)
|
||||
{
|
||||
found = true;
|
||||
actualHp = b.hp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(found);
|
||||
REQUIRE(actualHp == Approx(expectedHp));
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const Simulation sim(cfg, 42);
|
||||
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type != BuildingType::Hq) { continue; }
|
||||
// Rightmost body cell must be at x = -1 (asteroid right edge).
|
||||
int maxX = std::numeric_limits<int>::min();
|
||||
for (const QPoint& cell : b.bodyCells)
|
||||
{
|
||||
if (cell.x() > maxX) { maxX = cell.x(); }
|
||||
}
|
||||
REQUIRE(maxX == -1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const Simulation sim(cfg, 42);
|
||||
|
||||
int armedPlayerStations = 0;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::PlayerDefenceStation && b.weapon)
|
||||
{
|
||||
++armedPlayerStations;
|
||||
REQUIRE(b.weapon->damage > 0.0f);
|
||||
REQUIRE(b.weapon->range > 0.0f);
|
||||
REQUIRE(b.weapon->fireRateHz > 0.0f);
|
||||
}
|
||||
}
|
||||
REQUIRE(armedPlayerStations == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const Simulation sim(cfg, 42);
|
||||
|
||||
int armedEnemyStations = 0;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation && b.weapon)
|
||||
{
|
||||
++armedEnemyStations;
|
||||
REQUIRE(b.weapon->damage > 0.0f);
|
||||
REQUIRE(b.weapon->range > 0.0f);
|
||||
REQUIRE(b.weapon->fireRateHz > 0.0f);
|
||||
}
|
||||
}
|
||||
REQUIRE(armedEnemyStations == 2);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wave spawning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
// The maximum gap is gapMaxSeconds = 45s → 1350 ticks.
|
||||
// Run 1500 ticks to guarantee at least one wave has triggered.
|
||||
const int limit = static_cast<int>(secondsToTicks(50.0));
|
||||
for (int i = 0; i < limit; ++i)
|
||||
{
|
||||
sim.tick();
|
||||
}
|
||||
|
||||
bool foundEnemyShip = false;
|
||||
for (const Ship& s : sim.ships().allShips())
|
||||
{
|
||||
if (s.isEnemy)
|
||||
{
|
||||
foundEnemyShip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(foundEnemyShip);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: only eligible ships (cost > 0) appear in waves", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
// Run long enough for several waves.
|
||||
const int limit = static_cast<int>(secondsToTicks(120.0));
|
||||
for (int i = 0; i < limit; ++i)
|
||||
{
|
||||
sim.tick();
|
||||
}
|
||||
|
||||
for (const Ship& s : sim.ships().allShips())
|
||||
{
|
||||
if (!s.isEnemy) { continue; }
|
||||
// salvage_ship and repair_ship have cost_formula = "0" and must not spawn.
|
||||
REQUIRE(s.blueprintId != "salvage_ship");
|
||||
REQUIRE(s.blueprintId != "repair_ship");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Push
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
// Damage both enemy stations to 0.
|
||||
sim.buildings().forEachBuilding([](Building& b)
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
{
|
||||
b.hp = -1.0f;
|
||||
}
|
||||
});
|
||||
|
||||
sim.tick();
|
||||
|
||||
// After push: should have 2 new enemy stations.
|
||||
int enemyCount = 0;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation) { ++enemyCount; }
|
||||
}
|
||||
REQUIRE(enemyCount == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: push emits exactly one BlueprintDropEvent", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
sim.buildings().forEachBuilding([](Building& b)
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
{
|
||||
b.hp = -1.0f;
|
||||
}
|
||||
});
|
||||
|
||||
sim.tick();
|
||||
|
||||
const std::vector<BlueprintDropEvent> events = sim.drainBlueprintDropEvents();
|
||||
REQUIRE(events.size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: push blueprint drop awards a known ship id", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
sim.buildings().forEachBuilding([](Building& b)
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
{
|
||||
b.hp = -1.0f;
|
||||
}
|
||||
});
|
||||
|
||||
sim.tick();
|
||||
const std::vector<BlueprintDropEvent> events = sim.drainBlueprintDropEvents();
|
||||
REQUIRE(events.size() == 1);
|
||||
|
||||
bool validId = false;
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
if (def.id == events[0].blueprintId)
|
||||
{
|
||||
validId = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(validId);
|
||||
REQUIRE(events[0].newLevel >= 1);
|
||||
}
|
||||
|
||||
TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
// Record the X position of the initial enemy stations.
|
||||
int initialX = std::numeric_limits<int>::min();
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
{
|
||||
if (b.anchor.x() > initialX) { initialX = b.anchor.x(); }
|
||||
}
|
||||
}
|
||||
|
||||
sim.buildings().forEachBuilding([](Building& b)
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation) { b.hp = -1.0f; }
|
||||
});
|
||||
sim.tick();
|
||||
|
||||
int newX = std::numeric_limits<int>::min();
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.type == BuildingType::EnemyDefenceStation)
|
||||
{
|
||||
if (b.anchor.x() > newX) { newX = b.anchor.x(); }
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(newX > initialX);
|
||||
}
|
||||
Reference in New Issue
Block a user