#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 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(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 artillery; std::vector 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(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(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 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(events[0].kind), static_cast(eve::rts::LifecycleEventKind::SupplyDispatched)); CHECK_EQ(static_cast(events[1].kind), static_cast(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(events[0].kind), static_cast(eve::rts::LifecycleEventKind::AmmoResupplied)); CHECK_EQ(static_cast(events[1].kind), static_cast(eve::rts::LifecycleEventKind::SupplyReturning)); auto returnOrder = supplier->orders()->values.current(); REQUIRE(returnOrder.ok()); CHECK_EQ(static_cast(returnOrder.value().kind), static_cast(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(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::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::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 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(abilityEvents[0].kind), static_cast(eve::rts::LifecycleEventKind::AbilityChannelStarted)); CHECK_EQ(static_cast(abilityEvents[1].kind), static_cast(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>::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(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(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(supportOrder.value().kind), static_cast(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(counterOrder.value().kind), static_cast(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 threats; std::vector weapons; for (const auto& position : std::array{{{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(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(std::lround(selected.value().target.x)), static_cast(std::lround(selected.value().target.y)))); for (auto* weapon : weapons) weapon->release(); for (Unit* threat : threats) threat->release(); gun->release(); red->release(); blue->release(); }