Files

882 lines
43 KiB
C++

#include "RtsCompositionFixtures.h"
TEST_CASE("rts.autoCombatUsesCanonicalSensingWeaponAndDamage") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000031"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000032"));
Unit* attacker = Unit::createUnit(subject("00000000-0000-7000-8000-000000000033"));
Unit* target = Unit::createUnit(subject("00000000-0000-7000-8000-000000000034"));
auto blueLink = eve::rts::FactionLink::bind(ecs::handle_of(blue));
auto redLink = eve::rts::FactionLink::bind(ecs::handle_of(red));
REQUIRE(blueLink.ok());
REQUIRE(redLink.ok());
attacker->faction()->link = std::move(blueLink).takeValue();
target->faction()->link = std::move(redLink).takeValue();
attacker->motion()->x = 0.0f;
target->motion()->x = 5.0f;
attacker->durability()->state.health = 20.0;
attacker->durability()->state.maxHealth = 20.0;
target->durability()->state.health = 20.0;
target->durability()->state.maxHealth = 20.0;
target->shield()->capacity = 4.0f;
target->shield()->value = 4.0f;
target->shield()->regenDelay = 1.0f;
attacker->veterancy()->veteranThreshold = 20.0f;
attacker->veterancy()->eliteThreshold = 40.0f;
attacker->veterancy()->veteranDamageFactor = 1.25f;
attacker->veterancy()->eliteDamageFactor = 1.5f;
attacker->veterancy()->veteranHealthFactor = 1.2f;
attacker->veterancy()->eliteHealthFactor = 1.5f;
attacker->combat()->acquisitionRange = 10.0f;
attacker->combat()->leashRange = 12.0f;
attacker->combat()->suppressionPerShot = 3.0f;
target->morale()->capacity = 10.0f;
eve::weapon::WeaponEntity* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "rts-rifle";
definition.kind = eve::weapon::WeaponKind::Ranged;
definition.logic = "rts-hitscan";
definition.damage = 6.0f;
definition.damageType = "damage.physical";
definition.range = 8.0f;
definition.cooldown = 0.1f;
definition.magSize = 10;
definition.reserveSize = 10;
definition.reloadTime = 1.0f;
definition.blockedByObstacles = true;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
weapon->state()->stages = &weapon->definition()->def->stages;
weapon->state()->resource.kind = eve::weapon::ResourceKind::Ammo;
weapon->state()->resource.value = 10.0f;
weapon->state()->resource.max = 10.0f;
auto weaponLink = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponLink.ok());
attacker->weapon()->link = std::move(weaponLink).takeValue();
eve::sensing::SensingWorld sensing;
eve::combat::DamageRuntime damage;
eve::rts::CombatFireSystem::State combatState;
const eve::SimulationStep obstructedStep{eve::SimulationTick{1},
eve::Duration::fromSeconds(0.2).expect("obstructed combat dt")};
std::vector<eve::rts::CombatFireEvent> blockedEvents;
auto blockedLine = [&](eve::rts::WorldPosition origin, eve::rts::WorldPosition destination,
ecs::EntityHandle source, ecs::EntityHandle targetHandle,
const eve::weapon::WeaponDefinition& queriedWeapon) {
CHECK(std::abs(origin.x) < 1e-5f);
CHECK(std::abs(destination.x - 5.0f) < 1e-5f);
CHECK(ecs::try_get(source) == attacker);
CHECK(ecs::try_get(targetHandle) == target);
CHECK_EQ(queriedWeapon.id, "rts-rifle");
return eve::Result<bool>::success(false);
};
auto blockedSink = [&](const eve::rts::CombatFireEvent& event, eve::SimulationTick tick) {
CHECK_EQ(tick.value(), 1u);
blockedEvents.push_back(event);
};
auto obstructed = eve::rts::CombatFireSystem::step(obstructedStep, combatState, sensing, damage, nullptr,
blockedLine, nullptr, {}, {}, {}, blockedSink);
REQUIRE(obstructed.ok());
CHECK_EQ(obstructed.value(), 0u);
CHECK(std::abs(target->durability()->state.health - 20.0) < 1e-5);
CHECK(std::abs(weapon->state()->resource.value - 10.0f) < 1e-5f);
REQUIRE_EQ(blockedEvents.size(), 1u);
CHECK_EQ(static_cast<int>(blockedEvents[0].kind), static_cast<int>(eve::rts::CombatFireEventKind::FireBlocked));
CHECK(blockedEvents[0].source == attacker->identity()->subject);
CHECK(blockedEvents[0].target == target->identity()->subject);
auto stillObstructed = eve::rts::CombatFireSystem::step(obstructedStep, combatState, sensing, damage, nullptr,
blockedLine, nullptr, {}, {}, {}, blockedSink);
REQUIRE(stillObstructed.ok());
CHECK_EQ(blockedEvents.size(), 1u);
auto inaccurate = definition;
inaccurate.accuracy = 0.0f;
inaccurate.scatterRadius = 3.0f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(inaccurate);
weapon->definition()->def = weapon->definition()->owned.get();
weapon->state()->resource.value = 1.0f;
std::vector<eve::rts::CombatFireEvent> fireEvents;
auto missed = eve::rts::CombatFireSystem::step(
{eve::SimulationTick{2}, eve::Duration::fromSeconds(0.2).expect("miss dt")}, combatState, sensing, damage,
nullptr, {}, nullptr, {}, {}, {}, [&](const eve::rts::CombatFireEvent& event, eve::SimulationTick tick) {
CHECK_EQ(tick.value(), 2u);
fireEvents.push_back(event);
});
REQUIRE(missed.ok());
CHECK_EQ(missed.value(), 1u);
REQUIRE_EQ(fireEvents.size(), 3u);
CHECK_EQ(static_cast<int>(fireEvents[0].kind), static_cast<int>(eve::rts::CombatFireEventKind::WeaponFired));
CHECK_EQ(static_cast<int>(fireEvents[1].kind), static_cast<int>(eve::rts::CombatFireEventKind::ShotMissed));
CHECK_EQ(static_cast<int>(fireEvents[2].kind), static_cast<int>(eve::rts::CombatFireEventKind::WeaponDry));
CHECK(fireEvents[0].source == attacker->identity()->subject);
CHECK(fireEvents[0].target == target->identity()->subject);
CHECK(std::abs(fireEvents[0].point.x - fireEvents[1].point.x) < 1e-5f);
CHECK(std::abs(fireEvents[0].point.y - fireEvents[1].point.y) < 1e-5f);
CHECK(std::abs(target->durability()->state.health - 20.0) < 1e-5);
CHECK_EQ(attacker->combat()->shotSequence, 1u);
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
weapon->state()->resource.value = 0.0f;
weapon->state()->resource.reserve = 10;
weapon->state()->cooldown = 0.1f;
std::vector<eve::rts::CombatFireEvent> reloadEvents;
auto reloading = eve::rts::CombatFireSystem::step(
{eve::SimulationTick{3}, eve::Duration::fromSeconds(0.2).expect("reload dt")}, combatState, sensing, damage,
nullptr, {}, nullptr, {}, {}, {}, [&](const eve::rts::CombatFireEvent& event, eve::SimulationTick tick) {
CHECK_EQ(tick.value(), 3u);
reloadEvents.push_back(event);
});
REQUIRE(reloading.ok());
REQUIRE_EQ(reloadEvents.size(), 1u);
CHECK_EQ(static_cast<int>(reloadEvents[0].kind), static_cast<int>(eve::rts::CombatFireEventKind::ReloadStarted));
attacker->combat()->stance = eve::rts::CombatStance::Passive;
attacker->combat()->target = {};
reloadEvents.clear();
auto reloaded = eve::rts::CombatFireSystem::step(
{eve::SimulationTick{4}, eve::Duration::fromSeconds(0.8).expect("reload completion dt")}, combatState, sensing,
damage, nullptr, {}, nullptr, {}, {}, {},
[&](const eve::rts::CombatFireEvent& event, eve::SimulationTick tick) {
CHECK_EQ(tick.value(), 4u);
reloadEvents.push_back(event);
});
REQUIRE(reloaded.ok());
REQUIRE_EQ(reloadEvents.size(), 1u);
CHECK_EQ(static_cast<int>(reloadEvents[0].kind), static_cast<int>(eve::rts::CombatFireEventKind::ReloadCompleted));
attacker->combat()->stance = eve::rts::CombatStance::Defensive;
weapon->state()->resource.reloading = false;
weapon->state()->resource.reloadProgress = 0.0f;
weapon->state()->resource.value = 10.0f;
weapon->state()->cooldown = 0.0f;
for (std::uint64_t tick = 3; tick <= 6; ++tick) {
const eve::SimulationStep step{eve::SimulationTick{tick}, eve::Duration::fromSeconds(0.2).expect("combat dt")};
auto fired = eve::rts::CombatFireSystem::step(step, combatState, sensing, damage);
REQUIRE(fired.ok());
CHECK_EQ(fired.value(), 1u);
}
CHECK(!target->durability()->alive);
CHECK(std::abs(target->durability()->state.health) < 1e-5);
CHECK(std::abs(target->shield()->value) < 1e-5f);
CHECK_EQ(attacker->veterancy()->level, 1);
CHECK(std::abs(attacker->veterancy()->experience - 20.0f) < 1e-5f);
CHECK(std::abs(attacker->combat()->upgradeDamageFactor - 1.25f) < 1e-5f);
CHECK(std::abs(attacker->durability()->state.maxHealth - 24.0) < 1e-5);
CHECK(target->morale()->active);
CHECK(std::abs(target->morale()->suppression - 10.0f) < 1e-5f);
CHECK(ecs::try_get(attacker->combat()->target) == nullptr);
weapon->release();
attacker->release();
target->release();
blue->release();
red->release();
}
TEST_CASE("rts.shieldsRegenerateOnlyAfterDamageCooldown") {
ecs::Table world;
ecs::ScopedTable guard(world);
Unit* unit = Unit::createUnit();
unit->durability()->state.health = 10.0;
unit->durability()->state.maxHealth = 10.0;
unit->shield()->capacity = 10.0f;
unit->shield()->value = 2.0f;
unit->shield()->regenRate = 3.0f;
unit->shield()->regenDelay = 2.0f;
unit->shield()->cooldown = 2.0f;
auto first =
eve::rts::ShieldSystem::step({eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("shield dt")});
REQUIRE(first.ok());
CHECK(std::abs(unit->shield()->value - 2.0f) < 1e-5f);
auto second =
eve::rts::ShieldSystem::step({eve::SimulationTick{2}, eve::Duration::fromSeconds(1.0).expect("shield dt")});
REQUIRE(second.ok());
CHECK(std::abs(unit->shield()->value - 5.0f) < 1e-5f);
std::vector<eve::rts::LifecycleEvent> shieldEvents;
auto full =
eve::rts::ShieldSystem::step({eve::SimulationTick{3}, eve::Duration::fromSeconds(2.0).expect("shield full dt")},
[&](const eve::rts::LifecycleEvent& event, eve::SimulationTick tick) {
CHECK_EQ(tick.value(), 3u);
shieldEvents.push_back(event);
});
REQUIRE(full.ok());
CHECK_EQ(unit->shield()->value, 10.0f);
REQUIRE_EQ(shieldEvents.size(), 1u);
CHECK_EQ(static_cast<int>(shieldEvents[0].kind), static_cast<int>(eve::rts::LifecycleEventKind::ShieldRecharged));
unit->release();
}
TEST_CASE("rts.moraleAuraAcceleratesSuppressionRecovery") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* faction = Faction::createFaction();
Unit* suppressed = Unit::createUnit();
Unit* leader = Unit::createUnit();
auto suppressedFaction = eve::rts::FactionLink::bind(ecs::handle_of(faction));
auto leaderFaction = eve::rts::FactionLink::bind(ecs::handle_of(faction));
REQUIRE(suppressedFaction.ok());
REQUIRE(leaderFaction.ok());
suppressed->faction()->link = std::move(suppressedFaction).takeValue();
leader->faction()->link = std::move(leaderFaction).takeValue();
suppressed->morale()->capacity = 10.0f;
suppressed->morale()->suppression = 6.0f;
suppressed->morale()->recoveryRate = 1.0f;
suppressed->morale()->active = true;
leader->morale()->auraRange = 4.0f;
leader->morale()->auraRecoveryBonus = 2.0f;
const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("morale dt")};
std::vector<eve::rts::LifecycleEvent> moraleEvents;
auto recovered =
eve::rts::MoraleSystem::step(step, [&](const eve::rts::LifecycleEvent& event, eve::SimulationTick tick) {
CHECK_EQ(tick.value(), 1u);
moraleEvents.push_back(event);
});
REQUIRE(recovered.ok());
CHECK(std::abs(suppressed->morale()->suppression - 3.0f) < 1e-5f);
CHECK(!suppressed->morale()->active);
REQUIRE_EQ(moraleEvents.size(), 1u);
CHECK_EQ(static_cast<int>(moraleEvents[0].kind),
static_cast<int>(eve::rts::LifecycleEventKind::SuppressionRecovered));
suppressed->release();
leader->release();
faction->release();
}
TEST_CASE("rts.attackGroundFiresThroughCanonicalWeaponRuntime") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* faction = Faction::createFaction(subject("00000000-0000-7000-8000-000000000041"));
Unit* artillery = Unit::createUnit(subject("00000000-0000-7000-8000-000000000042"));
auto factionLink = eve::rts::FactionLink::bind(ecs::handle_of(faction));
REQUIRE(factionLink.ok());
artillery->faction()->link = std::move(factionLink).takeValue();
artillery->motion()->x = 0.0f;
eve::weapon::WeaponEntity* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "rts-howitzer";
definition.kind = eve::weapon::WeaponKind::Ranged;
definition.logic = "rts-indirect";
definition.range = 20.0f;
definition.cooldown = 0.1f;
definition.magSize = 4;
definition.projectile.speed = 12.0f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
weapon->state()->stages = &weapon->definition()->def->stages;
weapon->state()->resource.kind = eve::weapon::ResourceKind::Ammo;
weapon->state()->resource.value = 4.0f;
weapon->state()->resource.max = 4.0f;
auto weaponLink = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponLink.ok());
artillery->weapon()->link = std::move(weaponLink).takeValue();
CommandSpec attackGround;
attackGround.kind = OrderKind::AttackGround;
attackGround.target = {10.0f, 2.0f};
auto queued = artillery->orders()->values.enqueue(attackGround);
REQUIRE(queued.ok());
std::move(queued).takeValue();
eve::sensing::SensingWorld sensing;
eve::combat::DamageRuntime damage;
eve::rts::CombatFireSystem::State state;
const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.2).expect("artillery dt")};
auto fired = eve::rts::CombatFireSystem::step(step, state, sensing, damage);
REQUIRE(fired.ok());
CHECK_EQ(fired.value(), 1u);
CHECK(artillery->orders()->values.empty());
CHECK(std::abs(weapon->state()->resource.value - 3.0f) < 1e-5f);
weapon->release();
artillery->release();
faction->release();
}
TEST_CASE("rts.armedBuildingAcquiresAndFiresAtEnemyUnit") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000051"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000052"));
Building* turret = Building::createBuilding(subject("00000000-0000-7000-8000-000000000053"));
Unit* target = Unit::createUnit(subject("00000000-0000-7000-8000-000000000054"));
auto blueLink = eve::rts::FactionLink::bind(ecs::handle_of(blue));
auto redLink = eve::rts::FactionLink::bind(ecs::handle_of(red));
REQUIRE(blueLink.ok());
REQUIRE(redLink.ok());
turret->faction()->link = std::move(blueLink).takeValue();
target->faction()->link = std::move(redLink).takeValue();
turret->placement()->placed = true;
target->motion()->x = 4.0f;
target->durability()->state.health = 12.0;
target->durability()->state.maxHealth = 12.0;
turret->combat()->acquisitionRange = 8.0f;
eve::weapon::WeaponEntity* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "rts-turret";
definition.kind = eve::weapon::WeaponKind::Ranged;
definition.logic = "rts-hitscan";
definition.damage = 12.0f;
definition.range = 8.0f;
definition.magSize = 2;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
weapon->state()->stages = &weapon->definition()->def->stages;
weapon->state()->resource.kind = eve::weapon::ResourceKind::Ammo;
weapon->state()->resource.value = 2.0f;
weapon->state()->resource.max = 2.0f;
auto weaponLink = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponLink.ok());
turret->weapon()->link = std::move(weaponLink).takeValue();
eve::sensing::SensingWorld sensing;
eve::combat::DamageRuntime damage;
eve::rts::CombatFireSystem::State state;
const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.2).expect("turret dt")};
auto fired = eve::rts::CombatFireSystem::step(step, state, sensing, damage);
REQUIRE(fired.ok());
CHECK_EQ(fired.value(), 1u);
CHECK(!target->durability()->alive);
weapon->release();
turret->release();
target->release();
blue->release();
red->release();
}
TEST_CASE("rts.escortAndCombatGroupCoordinateTargets") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction();
Faction* red = Faction::createFaction();
auto protectedHandle = ecs::handle_of(Unit::createUnit());
auto escortHandle = ecs::handle_of(Unit::createUnit());
auto shooterAHandle = ecs::handle_of(Unit::createUnit());
auto shooterBHandle = ecs::handle_of(Unit::createUnit());
auto enemyAHandle = ecs::handle_of(Unit::createUnit());
auto enemyBHandle = ecs::handle_of(Unit::createUnit());
auto* protectedUnit = dynamic_cast<Unit*>(ecs::try_get(protectedHandle));
auto* escort = dynamic_cast<Unit*>(ecs::try_get(escortHandle));
auto* shooterA = dynamic_cast<Unit*>(ecs::try_get(shooterAHandle));
auto* shooterB = dynamic_cast<Unit*>(ecs::try_get(shooterBHandle));
auto* enemyA = dynamic_cast<Unit*>(ecs::try_get(enemyAHandle));
auto* enemyB = dynamic_cast<Unit*>(ecs::try_get(enemyBHandle));
auto bindFaction = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bindFaction(*protectedUnit, *blue);
bindFaction(*escort, *blue);
bindFaction(*shooterA, *blue);
bindFaction(*shooterB, *blue);
bindFaction(*enemyA, *red);
bindFaction(*enemyB, *red);
protectedUnit->motion()->x = 10.0f;
escort->tactics()->escortOffsetX = -2.0f;
escort->tactics()->protectionRange = 5.0f;
enemyA->motion()->x = 12.0f;
enemyA->durability()->state.health = 5.0;
enemyB->motion()->x = 6.0f;
enemyB->durability()->state.health = 5.0;
CommandSpec escortOrder;
escortOrder.kind = OrderKind::Escort;
escortOrder.targetEntity = ecs::handle_of(protectedUnit);
auto queued = escort->orders()->values.enqueue(escortOrder);
REQUIRE(queued.ok());
std::move(queued).takeValue();
eve::weapon::WeaponEntity* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "group-rifle";
definition.damage = 10.0f;
definition.range = 20.0f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
auto weaponA = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
auto weaponB = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponA.ok());
REQUIRE(weaponB.ok());
shooterA->weapon()->link = std::move(weaponA).takeValue();
shooterB->weapon()->link = std::move(weaponB).takeValue();
shooterA->tactics()->combatGroup = 7;
shooterB->tactics()->combatGroup = 7;
shooterA->combat()->acquisitionRange = 20.0f;
shooterB->combat()->acquisitionRange = 20.0f;
auto coordinated = eve::rts::TacticsSystem::step();
REQUIRE(coordinated.ok());
CHECK(std::abs(escort->tactics()->guardX - 8.0f) < 1e-5f);
CHECK(ecs::try_get(escort->combat()->target) == enemyA);
CHECK(ecs::try_get(shooterA->combat()->target) != nullptr);
CHECK(ecs::try_get(shooterB->combat()->target) != nullptr);
CHECK(ecs::try_get(shooterA->combat()->target) != ecs::try_get(shooterB->combat()->target));
weapon->release();
protectedUnit->release();
escort->release();
shooterA->release();
shooterB->release();
enemyA->release();
enemyB->release();
blue->release();
red->release();
}
TEST_CASE("rts.escortGroupPrioritizesDirectThreatAndDistributesInterceptors") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction();
Faction* red = Faction::createFaction();
Unit* protectedUnit = Unit::createUnit(subject("00000000-0000-7000-8000-000000000301"));
Unit* first = Unit::createUnit(subject("00000000-0000-7000-8000-000000000302"));
Unit* second = Unit::createUnit(subject("00000000-0000-7000-8000-000000000303"));
Unit* nearby = Unit::createUnit(subject("00000000-0000-7000-8000-000000000304"));
Unit* attacker = Unit::createUnit(subject("00000000-0000-7000-8000-000000000305"));
auto bind = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bind(*protectedUnit, *blue);
bind(*first, *blue);
bind(*second, *blue);
bind(*nearby, *red);
bind(*attacker, *red);
nearby->motion()->x = 1.0f;
attacker->motion()->x = 5.0f;
attacker->combat()->target = ecs::handle_of(protectedUnit);
for (Unit* escort : {first, second}) {
escort->tactics()->protectionRange = 8.0f;
CommandSpec order;
order.kind = OrderKind::Escort;
order.targetEntity = ecs::handle_of(protectedUnit);
REQUIRE(escort->orders()->values.enqueue(order).ok());
}
REQUIRE(eve::rts::TacticsSystem::step().ok());
CHECK(ecs::try_get(first->combat()->target) == attacker);
CHECK(ecs::try_get(second->combat()->target) == nearby);
protectedUnit->release();
first->release();
second->release();
nearby->release();
attacker->release();
blue->release();
red->release();
}
TEST_CASE("rts.escortProtectsWholeSupplyConvoyAndRotatesScreen") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction();
Faction* red = Faction::createFaction();
Unit* leader = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003b1"));
Unit* tail = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003b2"));
Unit* escort = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003b3"));
Unit* attacker = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003b4"));
auto bind = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bind(*leader, *blue);
bind(*tail, *blue);
bind(*escort, *blue);
bind(*attacker, *red);
leader->motion()->x = 0.0f;
tail->motion()->x = 10.0f;
leader->supply()->convoyLeader = leader->identity()->self;
tail->supply()->convoyLeader = leader->identity()->self;
CommandSpec supplyMission;
supplyMission.kind = OrderKind::SupplyRelay;
supplyMission.target = {20.0f, 0.0f};
supplyMission.targetEntity = tail->identity()->self;
REQUIRE(leader->orders()->values.replace(supplyMission).ok());
escort->tactics()->escortOffsetX = -2.0f;
escort->tactics()->protectionRange = 3.0f;
CommandSpec escortOrder;
escortOrder.kind = OrderKind::Escort;
escortOrder.targetEntity = leader->identity()->self;
REQUIRE(escort->orders()->values.replace(escortOrder).ok());
attacker->motion()->x = 11.0f;
attacker->combat()->target = tail->identity()->self;
REQUIRE(eve::rts::TacticsSystem::step().ok());
CHECK(std::abs(escort->combat()->guardX - 5.0f) < 1e-5f);
CHECK(std::abs(escort->tactics()->guardX - 3.0f) < 1e-5f);
CHECK(escort->combat()->leashRange >= 7.9f);
CHECK_EQ(ecs::try_get(escort->combat()->target), attacker);
attacker->release();
escort->release();
tail->release();
leader->release();
red->release();
blue->release();
}
TEST_CASE("rts.convoyEscortMatchesThreatSectorsAndReinforcesGap") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction();
Faction* red = Faction::createFaction();
Unit* leader = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003d1"));
Unit* tail = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003d2"));
Unit* vanguard = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003d3"));
Unit* rearguard = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003d4"));
Unit* frontThreat = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003d5"));
Unit* rearThreat = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003d6"));
auto bind = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
for (Unit* unit : {leader, tail, vanguard, rearguard}) bind(*unit, *blue);
bind(*frontThreat, *red);
bind(*rearThreat, *red);
leader->motion()->x = 4.0f;
tail->motion()->x = 0.0f;
leader->supply()->convoyLeader = leader->identity()->self;
tail->supply()->convoyLeader = leader->identity()->self;
CommandSpec mission;
mission.kind = OrderKind::SupplyRelay;
mission.targetEntity = tail->identity()->self;
mission.target = {20.0f, 0.0f};
REQUIRE(leader->orders()->values.replace(mission).ok());
vanguard->tactics()->escortOffsetX = 3.0f;
rearguard->tactics()->escortOffsetX = -3.0f;
for (Unit* escort : {vanguard, rearguard}) {
escort->tactics()->protectionRange = 12.0f;
CommandSpec order;
order.kind = OrderKind::Escort;
order.targetEntity = leader->identity()->self;
REQUIRE(escort->orders()->values.replace(order).ok());
}
frontThreat->motion()->x = 9.0f;
rearThreat->motion()->x = -5.0f;
REQUIRE(eve::rts::TacticsSystem::step().ok());
CHECK_EQ(ecs::try_get(vanguard->combat()->target), frontThreat);
CHECK_EQ(ecs::try_get(rearguard->combat()->target), rearThreat);
CHECK(vanguard->tactics()->escortSectorMatched);
CHECK(rearguard->tactics()->escortSectorMatched);
rearThreat->durability()->alive = false;
REQUIRE(eve::rts::TacticsSystem::step().ok());
CHECK_EQ(ecs::try_get(rearguard->combat()->target), frontThreat);
CHECK(rearguard->tactics()->escortReinforcing);
CHECK_EQ(rearguard->tactics()->escortReinforcementSector, 2);
rearThreat->release();
frontThreat->release();
rearguard->release();
vanguard->release();
tail->release();
leader->release();
red->release();
blue->release();
}
TEST_CASE("rts.escortsFormStableRearLineDuringSuppressionRetreat") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* faction = Faction::createFaction();
Unit* protectedUnit = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003e1"));
Unit* first = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003e2"));
Unit* second = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003e3"));
for (Unit* unit : {protectedUnit, first, second}) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(faction));
REQUIRE(link.ok());
unit->faction()->link = std::move(link).takeValue();
}
protectedUnit->motion()->x = 10.0f;
protectedUnit->morale()->retreating = true;
protectedUnit->navigation()->plannedGoal = {20.0f, 0.0f};
for (Unit* escort : {first, second}) {
escort->tactics()->escortOffsetX = -2.0f;
CommandSpec order;
order.kind = OrderKind::Escort;
order.targetEntity = protectedUnit->identity()->self;
REQUIRE(escort->orders()->values.replace(order).ok());
}
REQUIRE(eve::rts::TacticsSystem::step().ok());
CHECK(first->tactics()->escortRearGuard);
CHECK(second->tactics()->escortRearGuard);
CHECK(first->tactics()->guardX < protectedUnit->motion()->x);
CHECK(second->tactics()->guardX < protectedUnit->motion()->x);
CHECK_NE(first->tactics()->guardY, second->tactics()->guardY);
second->release();
first->release();
protectedUnit->release();
faction->release();
}
TEST_CASE("rts.retreatingCombatGroupAlternatesMovementAndCoverFire") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000325"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000326"));
Unit* first = Unit::createUnit(subject("00000000-0000-7000-8000-000000000327"));
Unit* second = Unit::createUnit(subject("00000000-0000-7000-8000-000000000328"));
Unit* enemy = Unit::createUnit(subject("00000000-0000-7000-8000-000000000329"));
auto bind = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bind(*first, *blue);
bind(*second, *blue);
bind(*enemy, *red);
first->motion()->y = -1.0f;
second->motion()->y = 1.0f;
enemy->motion()->x = -5.0f;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "retreat-rifle";
definition.damage = 1.0f;
definition.range = 20.0f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
for (Unit* unit : {first, second}) {
auto weaponLink = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponLink.ok());
unit->weapon()->link = std::move(weaponLink).takeValue();
unit->combat()->acquisitionRange = 20.0f;
unit->tactics()->combatGroup = 17;
unit->morale()->retreating = true;
unit->motion()->speed = 2.0f;
CommandSpec retreat;
retreat.kind = OrderKind::Move;
retreat.target = {20.0f, unit->motion()->y};
REQUIRE(unit->orders()->values.replace(retreat).ok());
}
const eve::SimulationStep firstPhase{eve::SimulationTick{1},
eve::Duration::fromSeconds(0.1).expect("retreat cover first phase")};
REQUIRE(eve::rts::TacticsSystem::step(nullptr, firstPhase).ok());
CHECK_EQ(first->tactics()->retreatFireTeam, 0);
CHECK_EQ(second->tactics()->retreatFireTeam, 1);
CHECK(first->tactics()->retreatCovering);
CHECK(!second->tactics()->retreatCovering);
CHECK_EQ(ecs::try_get(first->combat()->target), enemy);
CHECK_EQ(ecs::try_get(second->combat()->target), nullptr);
const float firstX = first->motion()->x;
const float secondX = second->motion()->x;
REQUIRE(eve::rts::MotionSystem::step(firstPhase).ok());
CHECK_EQ(first->motion()->x, firstX);
CHECK(second->motion()->x > secondX);
const eve::SimulationStep rotate{eve::SimulationTick{2},
eve::Duration::fromSeconds(1.4).expect("retreat cover rotation")};
REQUIRE(eve::rts::TacticsSystem::step(nullptr, rotate).ok());
CHECK(!first->tactics()->retreatCovering);
CHECK(second->tactics()->retreatCovering);
CHECK_EQ(ecs::try_get(first->combat()->target), nullptr);
CHECK_EQ(ecs::try_get(second->combat()->target), enemy);
const float firstBeforeRotate = first->motion()->x;
const float secondBeforeRotate = second->motion()->x;
REQUIRE(eve::rts::MotionSystem::step(firstPhase).ok());
CHECK(first->motion()->x > firstBeforeRotate);
CHECK_EQ(second->motion()->x, secondBeforeRotate);
weapon->release();
first->release();
second->release();
enemy->release();
blue->release();
red->release();
}
TEST_CASE("rts.combatGroupThreatSectorsPartitionTargetsAndFallBack") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction();
Faction* red = Faction::createFaction();
Unit* left = Unit::createUnit();
Unit* right = Unit::createUnit();
Unit* leftTarget = Unit::createUnit();
Unit* rightTarget = Unit::createUnit();
auto bind = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bind(*left, *blue);
bind(*right, *blue);
bind(*leftTarget, *red);
bind(*rightTarget, *red);
left->motion()->x = -1.0f;
right->motion()->x = 1.0f;
leftTarget->motion()->x = -5.0f;
rightTarget->motion()->x = 5.0f;
eve::weapon::WeaponEntity* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "sector-rifle";
definition.damage = 1.0f;
definition.range = 20.0f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
for (Unit* shooter : {left, right}) {
auto link = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(link.ok());
shooter->weapon()->link = std::move(link).takeValue();
shooter->combat()->acquisitionRange = 20.0f;
shooter->tactics()->combatGroup = 11;
}
left->tactics()->threatSector = -1;
right->tactics()->threatSector = 1;
REQUIRE(eve::rts::TacticsSystem::step().ok());
CHECK(ecs::try_get(left->combat()->target) == leftTarget);
CHECK(ecs::try_get(right->combat()->target) == rightTarget);
leftTarget->durability()->alive = false;
REQUIRE(eve::rts::TacticsSystem::step().ok());
CHECK(ecs::try_get(left->combat()->target) == rightTarget);
weapon->release();
left->release();
right->release();
leftTarget->release();
rightTarget->release();
blue->release();
red->release();
}
TEST_CASE("rts.combatFireControlMatchesDamageTypesToArmor") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000331"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000332"));
Unit* hunter = Unit::createUnit(subject("00000000-0000-7000-8000-000000000333"));
Unit* grenadier = Unit::createUnit(subject("00000000-0000-7000-8000-000000000334"));
Unit* armored = Unit::createUnit(subject("00000000-0000-7000-8000-000000000335"));
Unit* light = Unit::createUnit(subject("00000000-0000-7000-8000-000000000336"));
auto bind = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bind(*hunter, *blue);
bind(*grenadier, *blue);
bind(*armored, *red);
bind(*light, *red);
armored->motion()->x = light->motion()->x = 8.0f;
armored->motion()->y = -1.0f;
light->motion()->y = 1.0f;
for (Unit* target : {armored, light})
target->durability()->state.health = target->durability()->state.maxHealth = 100.0;
std::vector<eve::weapon::WeaponEntity*> weapons;
auto equip = [&](Unit& shooter, const char* id, const char* damageType) {
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = id;
definition.damage = 10.0f;
definition.damageType = damageType;
definition.range = 20.0f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
auto link = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(link.ok());
shooter.weapon()->link = std::move(link).takeValue();
shooter.combat()->acquisitionRange = 20.0f;
shooter.tactics()->combatGroup = 23;
weapons.push_back(weapon);
};
equip(*hunter, "tank-hunter", "damage.piercing");
equip(*grenadier, "grenadier", "damage.explosive");
FireControlArmorRule rule;
rule.armored = armored->identity()->subject;
rule.light = light->identity()->subject;
eve::combat::DamageRuntime damage(&rule);
REQUIRE(eve::rts::TacticsSystem::step(&damage).ok());
CHECK_EQ(ecs::try_get(hunter->combat()->target), armored);
CHECK_EQ(ecs::try_get(grenadier->combat()->target), light);
CHECK(std::abs(hunter->tactics()->fireControlEffectiveness - 2.0f) < 1e-5f);
CHECK(std::abs(grenadier->tactics()->fireControlEffectiveness - 2.0f) < 1e-5f);
armored->durability()->alive = false;
REQUIRE(eve::rts::TacticsSystem::step(&damage).ok());
CHECK_EQ(ecs::try_get(hunter->combat()->target), light);
CHECK(std::abs(hunter->tactics()->fireControlEffectiveness - 0.5f) < 1e-5f);
for (auto* weapon : weapons) weapon->release();
hunter->release();
grenadier->release();
armored->release();
light->release();
blue->release();
red->release();
}
TEST_CASE("rts.separateCombatGroupsShareAutomaticVolleyBudget") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000341"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000342"));
Unit* first = Unit::createUnit(subject("00000000-0000-7000-8000-000000000343"));
Unit* second = Unit::createUnit(subject("00000000-0000-7000-8000-000000000344"));
Unit* nearTarget = Unit::createUnit(subject("00000000-0000-7000-8000-000000000345"));
Unit* farTarget = Unit::createUnit(subject("00000000-0000-7000-8000-000000000346"));
auto bind = [&](Unit& unit, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bind(*first, *blue);
bind(*second, *blue);
bind(*nearTarget, *red);
bind(*farTarget, *red);
nearTarget->motion()->x = 5.0f;
farTarget->motion()->x = 7.0f;
for (Unit* target : {nearTarget, farTarget})
target->durability()->state.health = target->durability()->state.maxHealth = 10.0;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "shared-budget-cannon";
definition.damage = 10.0f;
definition.range = 20.0f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
std::uint64_t group = 1;
for (Unit* shooter : {first, second}) {
auto link = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(link.ok());
shooter->weapon()->link = std::move(link).takeValue();
shooter->combat()->acquisitionRange = 20.0f;
shooter->tactics()->combatGroup = group++;
}
eve::combat::DamageRuntime damage;
REQUIRE(eve::rts::TacticsSystem::step(&damage).ok());
CHECK_EQ(ecs::try_get(first->combat()->target), nearTarget);
CHECK_EQ(ecs::try_get(second->combat()->target), farTarget);
CommandSpec focus;
focus.kind = OrderKind::Attack;
focus.targetEntity = ecs::handle_of(nearTarget);
REQUIRE(first->orders()->values.replace(focus).ok());
REQUIRE(second->orders()->values.replace(focus).ok());
first->combat()->target = ecs::handle_of(nearTarget);
second->combat()->target = ecs::handle_of(nearTarget);
REQUIRE(eve::rts::TacticsSystem::step(&damage).ok());
CHECK_EQ(ecs::try_get(first->combat()->target), nearTarget);
CHECK_EQ(ecs::try_get(second->combat()->target), nearTarget);
weapon->release();
first->release();
second->release();
nearTarget->release();
farTarget->release();
blue->release();
red->release();
}