Files
EVEngine/test/rts_combat_2.cpp

858 lines
43 KiB
C++

#include "RtsCompositionFixtures.h"
TEST_CASE("rts.automaticTurretsShareDamageCommitmentsWhileExplicitAttackFocuses") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000351"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000352"));
Building* first = Building::createBuilding(subject("00000000-0000-7000-8000-000000000353"));
Building* second = Building::createBuilding(subject("00000000-0000-7000-8000-000000000354"));
Unit* nearTarget = Unit::createUnit(subject("00000000-0000-7000-8000-000000000355"));
Unit* farTarget = Unit::createUnit(subject("00000000-0000-7000-8000-000000000356"));
auto bindBuilding = [&](Building& building) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(blue));
REQUIRE(link.ok());
building.faction()->link = std::move(link).takeValue();
};
auto bindTarget = [&](Unit& unit) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(red));
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
unit.durability()->state.health = unit.durability()->state.maxHealth = 50.0;
};
bindBuilding(*first);
bindBuilding(*second);
bindTarget(*nearTarget);
bindTarget(*farTarget);
second->placement()->worldY = 1.0f;
nearTarget->motion()->x = 4.0f;
farTarget->motion()->x = 6.0f;
std::vector<eve::weapon::WeaponEntity*> weapons;
for (Building* turret : {first, second}) {
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "commitment-cannon";
definition.damage = 80.0f;
definition.range = 10.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());
turret->weapon()->link = std::move(link).takeValue();
turret->combat()->acquisitionRange = 10.0f;
weapons.push_back(weapon);
}
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);
for (auto* weapon : weapons) weapon->release();
first->release();
second->release();
nearTarget->release();
farTarget->release();
blue->release();
red->release();
}
TEST_CASE("rts.splashCommitmentsDistributeAutomaticFireAcrossHostileClusters") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000361"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000362"));
std::vector<Unit*> artillery;
std::vector<Unit*> targets;
for (const char* id : {"00000000-0000-7000-8000-000000000363", "00000000-0000-7000-8000-000000000364",
"00000000-0000-7000-8000-000000000365"})
artillery.push_back(Unit::createUnit(subject(id)));
for (const char* id : {"00000000-0000-7000-8000-000000000366", "00000000-0000-7000-8000-000000000367",
"00000000-0000-7000-8000-000000000368", "00000000-0000-7000-8000-000000000369"})
targets.push_back(Unit::createUnit(subject(id)));
Unit* ally = Unit::createUnit(subject("00000000-0000-7000-8000-00000000036a"));
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 : artillery) bind(*unit, *blue);
for (Unit* unit : targets) {
bind(*unit, *red);
unit->durability()->state.health = unit->durability()->state.maxHealth = 80.0;
}
bind(*ally, *blue);
targets[0]->motion()->x = targets[1]->motion()->x = 5.0f;
targets[0]->motion()->y = 0.0f;
targets[1]->motion()->y = 1.0f;
targets[2]->motion()->x = targets[3]->motion()->x = 9.0f;
targets[2]->motion()->y = 0.0f;
targets[3]->motion()->y = 1.0f;
ally->motion()->x = 5.0f;
ally->motion()->y = 0.5f;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "cluster-shell";
definition.damage = 100.0f;
definition.range = 12.0f;
definition.projectile.speed = 10.0f;
definition.projectile.aoe = 2.0f;
definition.splashMinimumDamageFactor = 0.1f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
for (Unit* shooter : artillery) {
auto link = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(link.ok());
shooter->weapon()->link = std::move(link).takeValue();
shooter->combat()->acquisitionRange = 12.0f;
shooter->tactics()->combatGroup = 29;
}
eve::combat::DamageRuntime damage;
REQUIRE(eve::rts::TacticsSystem::step(&damage).ok());
int firstCluster = 0;
int secondCluster = 0;
for (Unit* shooter : artillery) {
ecs::Entity* selected = ecs::try_get(shooter->combat()->target);
firstCluster += selected == targets[0] || selected == targets[1];
secondCluster += selected == targets[2] || selected == targets[3];
CHECK_NE(selected, ally);
}
CHECK_EQ(firstCluster, 2);
CHECK_EQ(secondCluster, 1);
weapon->release();
for (Unit* unit : artillery) unit->release();
for (Unit* unit : targets) unit->release();
ally->release();
blue->release();
red->release();
}
TEST_CASE("rts.autoSupplyRefillsCanonicalWeaponAmmo") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* faction = Faction::createFaction();
Unit* supplier = Unit::createUnit();
Unit* recipient = Unit::createUnit();
auto supplierFaction = eve::rts::FactionLink::bind(ecs::handle_of(faction));
auto recipientFaction = eve::rts::FactionLink::bind(ecs::handle_of(faction));
REQUIRE(supplierFaction.ok());
REQUIRE(recipientFaction.ok());
supplier->faction()->link = std::move(supplierFaction).takeValue();
recipient->faction()->link = std::move(recipientFaction).takeValue();
supplier->supply()->stock = 8.0f;
supplier->supply()->capacity = 8.0f;
supplier->supply()->range = 3.0f;
supplier->supply()->transferRate = 4.0f;
supplier->supply()->autoDispatch = true;
eve::weapon::WeaponEntity* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "supply-rifle";
definition.kind = eve::weapon::WeaponKind::Ranged;
definition.magSize = 10;
definition.reserveSize = 10;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
weapon->state()->resource.kind = eve::weapon::ResourceKind::Ammo;
weapon->state()->resource.value = 2.0f;
weapon->state()->resource.max = 10.0f;
weapon->state()->resource.reserve = 0;
auto weaponLink = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponLink.ok());
recipient->weapon()->link = std::move(weaponLink).takeValue();
const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("supply dt")};
std::vector<eve::rts::LifecycleEvent> events;
const eve::rts::LifecycleEventSink eventSink = [&](const eve::rts::LifecycleEvent& event,
eve::SimulationTick tick) {
CHECK_EQ(tick.value(), 1u);
events.push_back(event);
};
auto supplied = eve::rts::SupplySystem::step(step, {}, nullptr, {}, eventSink);
REQUIRE(supplied.ok());
CHECK(supplied.value() >= 2u);
CHECK(std::abs(weapon->state()->resource.value - 6.0f) < 1e-5f);
CHECK(std::abs(supplier->supply()->stock - 4.0f) < 1e-5f);
CHECK(std::abs(supplier->supply()->reservedStock - 4.0f) < 1e-5f);
CHECK(!supplier->orders()->values.empty());
REQUIRE_EQ(events.size(), 2u);
CHECK_EQ(static_cast<int>(events[0].kind), static_cast<int>(eve::rts::LifecycleEventKind::SupplyDispatched));
CHECK_EQ(static_cast<int>(events[1].kind), static_cast<int>(eve::rts::LifecycleEventKind::AmmoResupplied));
CHECK_EQ(events[1].value, 4.0);
events.clear();
supplied = eve::rts::SupplySystem::step(step, {}, nullptr, {}, eventSink);
REQUIRE(supplied.ok());
CHECK(std::abs(weapon->state()->resource.value - 10.0f) < 1e-5f);
CHECK(std::abs(supplier->supply()->reservedStock) < 1e-5f);
CHECK(supplier->supply()->returning);
REQUIRE_EQ(events.size(), 2u);
CHECK_EQ(static_cast<int>(events[0].kind), static_cast<int>(eve::rts::LifecycleEventKind::AmmoResupplied));
CHECK_EQ(static_cast<int>(events[1].kind), static_cast<int>(eve::rts::LifecycleEventKind::SupplyReturning));
auto returnOrder = supplier->orders()->values.current();
REQUIRE(returnOrder.ok());
CHECK_EQ(static_cast<int>(returnOrder.value().kind), static_cast<int>(OrderKind::Move));
weapon->release();
supplier->release();
recipient->release();
faction->release();
}
TEST_CASE("rts.attackMovePausesInWeaponRangeThenResumesAndGuardsDestination") {
ecs::Table world;
ecs::ScopedTable guard(world);
Unit* attacker = Unit::createUnit();
Unit* enemy = Unit::createUnit();
attacker->motion()->speed = 5.0f;
attacker->combat()->engagementRange = 3.0f;
attacker->combat()->target = ecs::handle_of(enemy);
enemy->motion()->x = 2.0f;
CommandSpec command;
command.kind = OrderKind::AttackMove;
command.target = {5.0f, 0.0f};
REQUIRE(attacker->orders()->values.enqueue(command).ok());
const eve::SimulationStep tick{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("attack move dt")};
REQUIRE(eve::rts::MotionSystem::step(tick).ok());
CHECK(std::abs(attacker->motion()->x) < 1e-5f);
attacker->combat()->target = {};
REQUIRE(eve::rts::MotionSystem::step(tick).ok());
CHECK(std::abs(attacker->motion()->x - 5.0f) < 1e-5f);
REQUIRE(eve::rts::MovementOrderSystem::step().ok());
CHECK(!attacker->orders()->values.empty());
attacker->release();
enemy->release();
}
TEST_CASE("rts.stopSettlesImmediatelyAndHoldAnchorsAutomaticCombat") {
ecs::Table world;
ecs::ScopedTable guard(world);
Unit* unit = Unit::createUnit();
unit->motion()->x = 4.0f;
unit->motion()->y = 2.0f;
unit->combat()->target = ecs::handle_of(unit);
CommandSpec hold;
hold.kind = OrderKind::HoldPosition;
REQUIRE(unit->orders()->values.replace(hold).ok());
REQUIRE(eve::rts::CommandStateSystem::step().ok());
CHECK(unit->combat()->holdPosition);
CHECK(unit->combat()->guardSet);
CHECK(std::abs(unit->combat()->guardX - 4.0f) < 1e-5f);
CommandSpec stop;
stop.kind = OrderKind::Stop;
REQUIRE(unit->orders()->values.replace(stop).ok());
auto stopped = eve::rts::CommandStateSystem::step();
REQUIRE(stopped.ok());
CHECK_EQ(stopped.value(), 1u);
CHECK(unit->orders()->values.empty());
CHECK(!unit->combat()->holdPosition);
CHECK(!unit->combat()->guardSet);
CHECK(unit->combat()->target.table == nullptr);
unit->release();
}
TEST_CASE("rts.matchTeamsDriveAbilityRelationsAndAutomaticCombatAlliances") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* alpha = Faction::createFaction(subject("00000000-0000-7000-8000-00000000d101"));
Faction* bravo = Faction::createFaction(subject("00000000-0000-7000-8000-00000000d102"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-00000000d103"));
Match* match = Match::createMatch(subject("00000000-0000-7000-8000-00000000d104"));
REQUIRE(eve::rts::MatchSystem::addParticipant(*match, *alpha, 1).ok());
REQUIRE(eve::rts::MatchSystem::addParticipant(*match, *bravo, 1).ok());
REQUIRE(eve::rts::MatchSystem::addParticipant(*match, *red, 2).ok());
CHECK(eve::rts::FactionRelationSystem::isAllied(alpha, bravo));
CHECK(!eve::rts::FactionRelationSystem::isAllied(alpha, red));
Unit* caster = Unit::createUnit(subject("00000000-0000-7000-8000-00000000d105"));
Unit* teammate = Unit::createUnit(subject("00000000-0000-7000-8000-00000000d106"));
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();
unit.durability()->state.health = unit.durability()->state.maxHealth = 10.0;
};
bind(*caster, *alpha);
bind(*teammate, *bravo);
teammate->motion()->x = 2.0f;
eve::combat::DamageRuntime damage;
eve::rts::AbilitySpec hostile;
hostile.id = "hostile";
hostile.target = eve::rts::AbilityTarget::Enemy;
hostile.range = 4.0f;
hostile.damage = 2.0f;
CHECK(!eve::rts::AbilitySystem::cast(*caster, hostile, ecs::handle_of(teammate), {}, damage).ok());
eve::weapon::WeaponEntity* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "team-rifle";
definition.kind = eve::weapon::WeaponKind::Ranged;
definition.damage = 3.0f;
definition.damageType = "damage.physical";
definition.range = 5.0f;
definition.cooldown = 0.1f;
definition.magSize = 4;
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 = weapon->state()->resource.max = 4.0f;
auto weaponLink = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponLink.ok());
caster->weapon()->link = std::move(weaponLink).takeValue();
caster->combat()->acquisitionRange = 5.0f;
eve::sensing::SensingWorld sensing;
eve::rts::CombatFireSystem::State combatState;
auto fired = eve::rts::CombatFireSystem::step(
{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.2).expect("team combat dt")}, combatState, sensing,
damage);
REQUIRE(fired.ok());
CHECK_EQ(fired.value(), 0u);
CHECK_EQ(teammate->durability()->state.health, 10.0);
eve::weapon::WeaponDefinition rocket;
rocket.id = "team-rocket";
rocket.damage = 5.0f;
rocket.damageType = "damage.explosive";
rocket.range = 5.0f;
rocket.projectile.speed = 2.0f;
eve::rts::RTSProjectileSystem projectiles;
REQUIRE(projectiles
.launch(caster->identity()->subject, caster->faction()->link.handle(), {}, ecs::handle_of(teammate),
{2.0f, 0.0f}, rocket)
.ok());
auto impact = projectiles.step(
{eve::SimulationTick{2}, eve::Duration::fromSeconds(1.0).expect("team projectile dt")}, damage);
REQUIRE(impact.ok());
CHECK_EQ(impact.value(), 1u);
CHECK_EQ(teammate->durability()->state.health, 10.0);
caster->command()->range = 5.0f;
caster->command()->capacity = 2;
teammate->command()->requiresCommand = true;
teammate->command()->cost = 1;
REQUIRE(eve::rts::CommandNetworkSystem::step().ok());
CHECK(teammate->command()->inCommand);
CHECK(ecs::try_get(teammate->command()->source) == caster);
Unit* enemy = Unit::createUnit(subject("00000000-0000-7000-8000-00000000d108"));
bind(*enemy, *red);
enemy->motion()->x = 3.0f;
alpha->intel()->enabled = true;
CHECK(!eve::rts::FactionIntelSystem::isTargetable(alpha, enemy->identity()->subject));
alpha->intel()->contacts.push_back(
{subject("ffffffff-ffff-7fff-bfff-ffffffffffff"), "remembered", {}, 0.0, false, false});
alpha->intel()->contacts.push_back({enemy->identity()->subject, "unit", {3.0f, 0.0f}, 1.0, false, false});
CHECK(!eve::rts::AbilitySystem::cast(*caster, hostile, ecs::handle_of(enemy), {}, damage).ok());
alpha->intel()->contacts.back().visible = true;
alpha->intel()->contacts.back().detected = true;
REQUIRE(eve::rts::AbilitySystem::cast(*caster, hostile, ecs::handle_of(enemy), {}, damage).ok());
CHECK_EQ(enemy->durability()->state.health, 8.0);
alpha->intel()->contacts.back().visible = false;
alpha->intel()->contacts.back().detected = false;
eve::rts::CommandSpec hiddenAttack;
hiddenAttack.kind = eve::rts::OrderKind::Attack;
hiddenAttack.targetEntity = ecs::handle_of(enemy);
REQUIRE(caster->orders()->values.replace(hiddenAttack).ok());
auto hiddenFire = eve::rts::CombatFireSystem::step(
{eve::SimulationTick{3}, eve::Duration::fromSeconds(0.2).expect("hidden target dt")}, combatState, sensing,
damage);
REQUIRE(hiddenFire.ok());
CHECK_EQ(hiddenFire.value(), 0u);
CHECK_EQ(enemy->durability()->state.health, 8.0);
Building* influence = Building::createBuilding(subject("00000000-0000-7000-8000-00000000d107"));
auto influenceOwner = eve::rts::FactionLink::bind(ecs::handle_of(alpha));
REQUIRE(influenceOwner.ok());
influence->faction()->link = std::move(influenceOwner).takeValue();
influence->placement()->placed = true;
influence->construction()->progress = 1.0f;
influence->integrity()->alive = true;
influence->infrastructure()->powered = true;
influence->infrastructure()->buildInfluenceRadius = 5.0f;
const auto outpost = eve::LogicalId::fromParts("building", "outpost");
REQUIRE(outpost.has_value());
CHECK(eve::rts::BuildInfluenceSystem::validate(
*bravo, {2.0f, 0.0f}, *outpost,
[](eve::rts::WorldPosition, eve::LogicalId) { return eve::Result<void>::success(); })
.ok());
influence->release();
enemy->release();
weapon->release();
teammate->release();
caster->release();
match->release();
red->release();
bravo->release();
alpha->release();
}
TEST_CASE("rts.abilitiesUseCanonicalDamageEconomyCooldownAndInterruptibleChannels") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000231"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000232"));
Unit* caster = Unit::createUnit(subject("00000000-0000-7000-8000-000000000233"));
Unit* target = Unit::createUnit(subject("00000000-0000-7000-8000-000000000234"));
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());
caster->faction()->link = std::move(blueLink).takeValue();
target->faction()->link = std::move(redLink).takeValue();
caster->durability()->state.health = caster->durability()->state.maxHealth = 10.0;
target->durability()->state.health = target->durability()->state.maxHealth = 10.0;
target->motion()->x = 3.0f;
target->shield()->capacity = target->shield()->value = 3.0f;
target->shield()->regenDelay = 2.0f;
eve::combat::DamageRuntime damage;
eve::rts::AbilitySpec bolt;
bolt.id = "arc-bolt";
bolt.target = eve::rts::AbilityTarget::Enemy;
bolt.range = 5.0f;
bolt.cooldown = 1.0f;
bolt.resourceType = "energy";
bolt.resourceCost = 2;
bolt.damage = 5.0f;
std::int64_t debited = 0;
auto cast = eve::rts::AbilitySystem::cast(
*caster, bolt, ecs::handle_of(target), {}, damage, [&](Unit& source, const eve::resource::CostSpec& cost) {
CHECK(&source == caster);
debited += cost.items()[0].amount.value();
return eve::Result<eve::resource::Receipt>::success(eve::resource::Receipt{});
});
REQUIRE(cast.ok());
CHECK_EQ(debited, 2);
CHECK(std::abs(target->shield()->value) < 1e-5f);
CHECK(std::abs(target->durability()->state.health - 8.0) < 1e-5);
auto cooling = eve::rts::AbilitySystem::cast(*caster, bolt, ecs::handle_of(target), {}, damage);
CHECK(!cooling.ok());
eve::rts::AbilitySpec channel;
channel.id = "repair-channel";
channel.target = eve::rts::AbilityTarget::Self;
channel.range = 0.0f;
channel.castTime = 1.0f;
channel.healing = 4.0f;
channel.interruptOnDamage = true;
caster->durability()->state.health = 5.0;
std::vector<eve::rts::LifecycleEvent> abilityEvents;
auto collectAbility = [&](const eve::rts::LifecycleEvent& event, eve::SimulationTick) {
abilityEvents.push_back(event);
};
REQUIRE(eve::rts::AbilitySystem::cast(*caster, channel, {}, {}, damage, {}, {}, {}, collectAbility).ok());
caster->durability()->state.health = 4.0;
auto interrupted = eve::rts::AbilitySystem::step(
{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("ability dt")}, damage, {}, collectAbility);
REQUIRE(interrupted.ok());
CHECK(!caster->abilities()->channel.has_value());
CHECK(std::abs(caster->durability()->state.health - 4.0) < 1e-5);
REQUIRE_EQ(abilityEvents.size(), 2u);
CHECK_EQ(static_cast<int>(abilityEvents[0].kind),
static_cast<int>(eve::rts::LifecycleEventKind::AbilityChannelStarted));
CHECK_EQ(static_cast<int>(abilityEvents[1].kind),
static_cast<int>(eve::rts::LifecycleEventKind::AbilityInterrupted));
caster->veterancy()->veteranThreshold = 10.0f;
caster->veterancy()->eliteThreshold = 20.0f;
caster->veterancy()->veteranDamageFactor = 1.25f;
caster->veterancy()->eliteDamageFactor = 1.5f;
caster->veterancy()->veteranHealthFactor = 1.2f;
caster->veterancy()->eliteHealthFactor = 1.5f;
target->durability()->alive = true;
target->durability()->state.health = 2.0;
target->durability()->state.maxHealth = 10.0;
target->shield()->value = 0.0f;
eve::rts::AbilitySpec finisher;
finisher.id = "finisher";
finisher.target = eve::rts::AbilityTarget::Enemy;
finisher.range = 5.0f;
finisher.damage = 2.0f;
REQUIRE(eve::rts::AbilitySystem::cast(*caster, finisher, ecs::handle_of(target), {}, damage).ok());
CHECK(!target->durability()->alive);
CHECK_EQ(caster->veterancy()->experience, 10.0f);
CHECK_EQ(caster->veterancy()->level, 1);
CHECK_EQ(caster->combat()->upgradeDamageFactor, 1.25f);
CHECK_EQ(caster->durability()->state.maxHealth, 12.0);
target->release();
caster->release();
red->release();
blue->release();
}
TEST_CASE("rts.projectilesUseCanonicalTrajectorySweptImpactAndHostileSplashDamage") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000241"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000242"));
Unit* source = Unit::createUnit(subject("00000000-0000-7000-8000-000000000243"));
Unit* target = Unit::createUnit(subject("00000000-0000-7000-8000-000000000244"));
Unit* splash = Unit::createUnit(subject("00000000-0000-7000-8000-000000000245"));
Unit* ally = Unit::createUnit(subject("00000000-0000-7000-8000-000000000246"));
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();
unit.durability()->state.health = unit.durability()->state.maxHealth = 10.0;
};
bind(*source, *blue);
bind(*ally, *blue);
bind(*target, *red);
bind(*splash, *red);
target->motion()->x = 4.0f;
splash->motion()->x = 4.5f;
ally->motion()->x = 4.5f;
target->shield()->capacity = target->shield()->value = 2.0f;
eve::weapon::WeaponDefinition definition;
definition.id = "rts-rocket";
definition.damage = 6.0f;
definition.damageType = "damage.explosive";
definition.range = 10.0f;
definition.projectile.speed = 4.0f;
definition.projectile.aoe = 2.0f;
eve::rts::RTSProjectileSystem projectiles;
REQUIRE(projectiles
.launch(source->identity()->subject, source->faction()->link.handle(), {}, ecs::handle_of(target),
{4.0f, 0.0f}, definition)
.ok());
eve::combat::DamageRuntime damage;
auto flying =
projectiles.step({eve::SimulationTick{1}, eve::Duration::fromSeconds(0.5).expect("projectile dt")}, damage);
REQUIRE(flying.ok());
CHECK_EQ(flying.value(), 0u);
CHECK_EQ(projectiles.activeCount(), 1u);
const auto inFlight = projectiles.snapshot();
eve::rts::RTSProjectileSystem restoredProjectiles;
REQUIRE(restoredProjectiles
.restore(inFlight,
[&](eve::SubjectRef stable) -> ecs::Entity* {
for (Unit* unit : {source, target, splash, ally})
if (unit->identity()->subject == stable) return unit;
if (blue->identity()->subject == stable) return blue;
if (red->identity()->subject == stable) return red;
return nullptr;
})
.ok());
CHECK_EQ(restoredProjectiles.activeCount(), 1u);
auto impacted = restoredProjectiles.step(
{eve::SimulationTick{2}, eve::Duration::fromSeconds(0.5).expect("projectile dt")}, damage);
REQUIRE(impacted.ok());
CHECK_EQ(impacted.value(), 1u);
CHECK_EQ(restoredProjectiles.activeCount(), 0u);
CHECK(std::abs(target->shield()->value) < 1e-5f);
CHECK(std::abs(target->durability()->state.health - 6.0) < 1e-5);
CHECK(std::abs(splash->durability()->state.health - 5.5) < 1e-5);
CHECK(std::abs(ally->durability()->state.health - 10.0) < 1e-5);
Unit* blocker = Unit::createUnit(subject("00000000-0000-7000-8000-000000000247"));
bind(*blocker, *red);
blocker->motion()->x = 1.5f;
target->durability()->state.health = 10.0;
definition.projectile.aoe = 0.0f;
definition.blockedByObstacles = true;
REQUIRE(restoredProjectiles
.launch(source->identity()->subject, source->faction()->link.handle(), {}, ecs::handle_of(target),
{4.0f, 0.0f}, definition)
.ok());
bool queriedCollision = false;
auto blocked = restoredProjectiles.step(
{eve::SimulationTick{3}, eve::Duration::fromSeconds(0.5).expect("blocked projectile dt")}, damage,
[&](eve::rts::WorldPosition from, float fromHeight, eve::rts::WorldPosition to, float toHeight,
eve::SubjectRef projectileSource, ecs::EntityHandle intended) {
queriedCollision = true;
CHECK(from.x <= 1e-5f);
CHECK(to.x >= 1.9f);
CHECK(std::abs(fromHeight) < 1e-5f);
CHECK(std::abs(toHeight) < 1e-5f);
CHECK(projectileSource == source->identity()->subject);
CHECK(ecs::try_get(intended) == target);
return eve::Result<std::optional<eve::rts::ProjectileCollision>>::success(
eve::rts::ProjectileCollision{{1.5f, 0.0f}, ecs::handle_of(blocker)});
});
REQUIRE(blocked.ok());
CHECK(queriedCollision);
CHECK_EQ(blocked.value(), 1u);
CHECK_EQ(restoredProjectiles.activeCount(), 0u);
CHECK(std::abs(blocker->durability()->state.health - 4.0) < 1e-5);
CHECK(std::abs(target->durability()->state.health - 10.0) < 1e-5);
source->veterancy()->veteranThreshold = 10.0f;
source->veterancy()->eliteThreshold = 20.0f;
source->veterancy()->veteranDamageFactor = 1.25f;
source->veterancy()->eliteDamageFactor = 1.5f;
source->veterancy()->veteranHealthFactor = 1.2f;
source->veterancy()->eliteHealthFactor = 1.5f;
target->durability()->alive = true;
target->durability()->state.health = target->durability()->state.maxHealth = 10.0;
definition.damage = 12.0f;
definition.blockedByObstacles = false;
REQUIRE(restoredProjectiles
.launch(source->identity()->subject, source->faction()->link.handle(), {}, ecs::handle_of(target),
{4.0f, 0.0f}, definition)
.ok());
auto lethal = restoredProjectiles.step(
{eve::SimulationTick{4}, eve::Duration::fromSeconds(1.0).expect("lethal projectile dt")}, damage);
REQUIRE(lethal.ok());
CHECK(!target->durability()->alive);
CHECK_EQ(source->veterancy()->level, 1);
CHECK_EQ(source->veterancy()->experience, 10.0f);
CHECK_EQ(source->combat()->upgradeDamageFactor, 1.25f);
CHECK_EQ(source->durability()->state.maxHealth, 12.0);
blocker->release();
ally->release();
splash->release();
target->release();
source->release();
red->release();
blue->release();
}
TEST_CASE("rts.indirectFireRequiresFriendlyObserverAndFreezesAimAfterContactLoss") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-0000000003a1"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-0000000003a2"));
Unit* gun = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003a3"));
Unit* observer = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003a4"));
Unit* target = Unit::createUnit(subject("00000000-0000-7000-8000-0000000003a5"));
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(*gun, *blue);
bind(*observer, *blue);
bind(*target, *red);
gun->vision()->sightRange = 3.0f;
gun->combat()->acquisitionRange = 15.0f;
observer->motion()->x = 10.0f;
observer->vision()->sightRange = 4.0f;
target->motion()->x = 12.0f;
target->durability()->state.health = target->durability()->state.maxHealth = 100.0;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "observed-howitzer";
definition.kind = eve::weapon::WeaponKind::Ranged;
definition.logic = "rts-indirect";
definition.damage = 10.0f;
definition.range = 15.0f;
definition.cooldown = 0.1f;
definition.magSize = 4;
definition.projectile.speed = 2.0f;
definition.projectile.gravity = 1.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());
gun->weapon()->link = std::move(weaponLink).takeValue();
eve::sensing::SensingWorld sensing;
eve::combat::DamageRuntime damage;
eve::rts::CombatFireSystem::State combatState;
eve::rts::RTSProjectileSystem projectiles;
auto step =
eve::SimulationStep{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.1).expect("observed fire step")};
auto fired = eve::rts::CombatFireSystem::step(step, combatState, sensing, damage, &projectiles);
REQUIRE(fired.ok());
CHECK_EQ(ecs::try_get(gun->combat()->target), target);
CHECK_EQ(fired.value(), 1u);
CHECK(gun->artillery()->usingObservedFire);
CHECK_EQ(ecs::try_get(gun->artillery()->observedFireSpotter), observer);
auto inFlight = projectiles.snapshot();
REQUIRE_EQ(inFlight.payloads.size(), 1u);
CHECK_EQ(inFlight.payloads.front().target, target->identity()->subject);
CHECK_EQ(inFlight.payloads.front().observer, observer->identity()->subject);
eve::rts::RTSProjectileSystem restoredProjectiles;
REQUIRE(restoredProjectiles
.restore(inFlight,
[&](eve::SubjectRef stable) -> ecs::Entity* {
for (Unit* unit : {gun, observer, target})
if (unit->identity()->subject == stable) return unit;
if (blue->identity()->subject == stable) return blue;
if (red->identity()->subject == stable) return red;
return nullptr;
})
.ok());
observer->motion()->x = 30.0f;
target->motion()->x = 14.0f;
auto advanced = restoredProjectiles.step(step, damage);
REQUIRE(advanced.ok());
inFlight = restoredProjectiles.snapshot();
REQUIRE_EQ(inFlight.payloads.size(), 1u);
CHECK(!inFlight.payloads.front().target.isValid());
CHECK(!inFlight.payloads.front().observer.isValid());
CHECK(std::abs(inFlight.payloads.front().targetPoint.x - 12.0f) < 1e-5f);
gun->combat()->target = {};
step.tick = eve::SimulationTick{2};
auto blind = eve::rts::CombatFireSystem::step(step, combatState, sensing, damage, &projectiles);
REQUIRE(blind.ok());
CHECK_EQ(blind.value(), 0u);
CHECK_EQ(ecs::try_get(gun->combat()->target), nullptr);
weapon->release();
gun->release();
observer->release();
target->release();
blue->release();
red->release();
}
TEST_CASE("rts.fireSupportAssignsSuppressionAndCounterBatteryUsesRecentExposure") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000251"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000252"));
Unit* requester = Unit::createUnit(subject("00000000-0000-7000-8000-000000000253"));
Unit* battery = Unit::createUnit(subject("00000000-0000-7000-8000-000000000254"));
Unit* hostile = Unit::createUnit(subject("00000000-0000-7000-8000-000000000255"));
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();
unit.durability()->state.health = unit.durability()->state.maxHealth = 10.0;
};
bind(*requester, *blue);
bind(*battery, *blue);
bind(*hostile, *red);
hostile->motion()->x = 8.0f;
eve::weapon::WeaponDefinition indirect;
indirect.id = "rts-howitzer-support";
indirect.range = 20.0f;
indirect.projectile.speed = 10.0f;
indirect.projectile.gravity = 1.0f;
auto makeWeapon = [&]() {
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(indirect);
weapon->definition()->def = weapon->definition()->owned.get();
weapon->state()->stages = &weapon->definition()->def->stages;
return weapon;
};
auto* batteryWeapon = makeWeapon();
auto* hostileWeapon = makeWeapon();
auto batteryLink = eve::rts::WeaponLink::bind(ecs::handle_of(batteryWeapon));
auto hostileLink = eve::rts::WeaponLink::bind(ecs::handle_of(hostileWeapon));
REQUIRE(batteryLink.ok());
REQUIRE(hostileLink.ok());
battery->weapon()->link = std::move(batteryLink).takeValue();
hostile->weapon()->link = std::move(hostileLink).takeValue();
auto assigned = eve::rts::FireSupportSystem::request(*requester, {10.0f, 0.0f}, 2.0f, 3, 1);
REQUIRE(assigned.ok());
CHECK_EQ(assigned.value(), 1u);
auto supportOrder = battery->orders()->values.current();
REQUIRE(supportOrder.ok());
CHECK_EQ(static_cast<int>(supportOrder.value().kind), static_cast<int>(OrderKind::SuppressArea));
CHECK_EQ(battery->artillery()->suppressionShotsRemaining, 3);
CHECK(ecs::try_get(battery->artillery()->fireSupportRequester) == requester);
auto cancelled = eve::rts::FireSupportSystem::cancel(*requester);
REQUIRE(cancelled.ok());
CHECK_EQ(cancelled.value(), 1u);
battery->artillery()->autoCounterBattery = true;
battery->artillery()->counterBatteryWindowTicks = 5;
hostile->artillery()->lastFireTick = eve::SimulationTick{10};
hostile->artillery()->lastFirePosition = {8.0f, 0.0f};
auto countered = eve::rts::FireSupportSystem::step({eve::SimulationTick{12}, eve::Duration::zero()});
REQUIRE(countered.ok());
CHECK_EQ(countered.value(), 1u);
auto counterOrder = battery->orders()->values.current();
REQUIRE(counterOrder.ok());
CHECK_EQ(static_cast<int>(counterOrder.value().kind), static_cast<int>(OrderKind::AttackGround));
CHECK(std::abs(counterOrder.value().target.x - 8.0f) < 1e-5f);
hostileWeapon->release();
batteryWeapon->release();
hostile->release();
battery->release();
requester->release();
red->release();
blue->release();
}
TEST_CASE("rts.artilleryRelocationChoosesReachableLowThreatFreshPosition") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000291"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000292"));
Unit* gun = Unit::createUnit(subject("00000000-0000-7000-8000-000000000293"));
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();
unit.durability()->state.health = unit.durability()->state.maxHealth = 10.0;
};
bind(*gun, *blue);
gun->motion()->x = 10.0f;
gun->motion()->y = 10.0f;
eve::map::Pathfinder pathfinder(32, 32);
eve::rts::NavigationGrid grid;
auto initial = eve::rts::ArtilleryRelocationSystem::select(*gun, {20.0f, 10.0f}, 4.0f, 20.0f, pathfinder, grid);
REQUIRE(initial.ok());
const WorldPosition departed = initial.value().target;
gun->artillery()->departedPosition = departed;
gun->artillery()->hasDepartedPosition = true;
std::vector<Unit*> threats;
std::vector<eve::weapon::WeaponEntity*> weapons;
for (const auto& position : std::array<WorldPosition, 2>{{{9.03f, 13.88f}, {9.03f, 6.12f}}}) {
Unit* threat = Unit::createUnit();
bind(*threat, *red);
threat->motion()->x = position.x;
threat->motion()->y = position.y;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "relocation-threat";
definition.damage = 10.0f;
definition.range = 1.25f;
weapon->definition()->owned = std::make_shared<const eve::weapon::WeaponDefinition>(definition);
weapon->definition()->def = weapon->definition()->owned.get();
auto weaponLink = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(weaponLink.ok());
threat->weapon()->link = std::move(weaponLink).takeValue();
threats.push_back(threat);
weapons.push_back(weapon);
}
auto selected = eve::rts::ArtilleryRelocationSystem::select(*gun, {20.0f, 10.0f}, 4.0f, 20.0f, pathfinder, grid);
REQUIRE(selected.ok());
CHECK(selected.value().avoidedThreat);
CHECK(selected.value().avoidedDepartedPosition);
CHECK(std::hypot(selected.value().target.x - departed.x, selected.value().target.y - departed.y) > 1.0f);
CHECK(pathfinder.isWalkable(static_cast<int>(std::lround(selected.value().target.x)),
static_cast<int>(std::lround(selected.value().target.y))));
for (auto* weapon : weapons) weapon->release();
for (Unit* threat : threats) threat->release();
gun->release();
red->release();
blue->release();
}