Files
EVEngine/test/rts_combat_3.cpp

390 lines
21 KiB
C++

#include "RtsCompositionFixtures.h"
TEST_CASE("rts.suppressionAndEscortFacadesRoundTripDedicatedTacticalState") {
ecs::Table world;
ecs::ScopedTable guard(world);
eve::rts::RTS module;
const auto factionSubject = subject("00000000-0000-7000-8000-00000000d211");
const auto firstSubject = subject("00000000-0000-7000-8000-00000000d212");
const auto secondSubject = subject("00000000-0000-7000-8000-00000000d213");
const auto protectedSubject = subject("00000000-0000-7000-8000-00000000d214");
auto* faction = module.newFaction(factionSubject).value();
auto* first = module.newFactionUnit(*faction, firstSubject).value();
auto* second = module.newFactionUnit(*faction, secondSubject).value();
auto* protectedUnit = module.newFactionUnit(*faction, protectedSubject).value();
protectedUnit->motion()->x = 10.0f;
protectedUnit->motion()->y = 8.0f;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
auto firstWeapon = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
auto secondWeapon = eve::rts::WeaponLink::bind(ecs::handle_of(weapon));
REQUIRE(firstWeapon.ok());
REQUIRE(secondWeapon.ok());
first->weapon()->link = std::move(firstWeapon).takeValue();
second->weapon()->link = std::move(secondWeapon).takeValue();
eve::rts::RTSReplayCommand suppress;
suppress.tick = eve::SimulationTick{4};
suppress.operation = eve::rts::RTSReplayOperation::SuppressArea;
suppress.units = {secondSubject, firstSubject};
suppress.command.kind = OrderKind::SuppressArea;
suppress.command.target = {12.0f, 3.0f};
suppress.command.secondaryTarget = {18.0f, 7.0f};
suppress.command.radius = 2.0f;
suppress.priority = 3;
eve::rts::RTSReplayCommand escort;
escort.tick = eve::SimulationTick{5};
escort.operation = eve::rts::RTSReplayOperation::Escort;
escort.units = {secondSubject, firstSubject};
escort.targetEntity = protectedSubject;
escort.command.kind = OrderKind::Escort;
escort.command.radius = 6.0f;
escort.formation.spacing = 1.5f;
eve::rts::RTSCommandLog recorded;
REQUIRE(recorded.queue(suppress).ok());
REQUIRE(recorded.queue(escort).ok());
const std::string text = recorded.exportText();
CHECK(text.starts_with("EVERTS_COMMANDS 2\n"));
eve::rts::RTSCommandLog replay;
REQUIRE(replay.importText(text).ok());
CHECK_EQ(replay.exportText(), text);
auto suppressed = replay.apply(eve::SimulationTick{4}, module);
REQUIRE(suppressed.ok());
CHECK_EQ(suppressed.value(), std::size_t{2});
auto suppressionOrder = first->orders()->values.current();
REQUIRE(suppressionOrder.ok());
CHECK_EQ(static_cast<int>(suppressionOrder.value().kind), static_cast<int>(OrderKind::SuppressArea));
CHECK_EQ(first->artillery()->suppressionShotsRemaining, 3);
CHECK_EQ(second->artillery()->suppressionShotsRemaining, 3);
CHECK(std::abs(suppressionOrder.value().secondaryTarget.x - 18.0f) < 1e-5f);
auto escorted = replay.apply(eve::SimulationTick{5}, module);
REQUIRE(escorted.ok());
CHECK_EQ(escorted.value(), std::size_t{2});
auto escortOrder = first->orders()->values.current();
REQUIRE(escortOrder.ok());
CHECK_EQ(static_cast<int>(escortOrder.value().kind), static_cast<int>(OrderKind::Escort));
CHECK(ecs::try_get(escortOrder.value().targetEntity) == protectedUnit);
CHECK(first->tactics()->escortOffsetX > 0.0f);
CHECK(second->tactics()->escortOffsetX < 0.0f);
CHECK(std::abs(first->tactics()->protectionRange - 6.0f) < 1e-5f);
CHECK(std::abs(first->combat()->leashRange - 6.0f) < 1e-5f);
weapon->release();
}
TEST_CASE("rts.automaticTargetPoliciesAndWeaponPreferencesOverrideDistanceButExplicitAttackStillWins") {
ecs::Table world;
ecs::ScopedTable guard(world);
auto blueHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002a4")));
auto redHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002a5")));
auto workerDefinition = eve::LogicalId::parse("rts:worker");
auto tankDefinition = eve::LogicalId::parse("rts:tank");
REQUIRE(workerDefinition.has_value());
REQUIRE(tankDefinition.has_value());
auto attackerHandle =
ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002a1"), *workerDefinition));
auto closeHandle =
ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002a2"), *workerDefinition));
auto valuableHandle =
ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002a3"), *tankDefinition));
auto* blue = dynamic_cast<Faction*>(ecs::try_get(blueHandle));
auto* red = dynamic_cast<Faction*>(ecs::try_get(redHandle));
auto* attacker = dynamic_cast<Unit*>(ecs::try_get(attackerHandle));
auto* close = dynamic_cast<Unit*>(ecs::try_get(closeHandle));
auto* valuable = dynamic_cast<Unit*>(ecs::try_get(valuableHandle));
auto bindFaction = [&](Unit& unit, ecs::EntityHandle faction) {
auto link = eve::rts::FactionLink::bind(faction);
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bindFaction(*attacker, blueHandle);
bindFaction(*close, redHandle);
bindFaction(*valuable, redHandle);
attacker->combat()->acquisitionRange = 10.0f;
attacker->combat()->targetPriorities[tankDefinition->format()] = 5.0f;
close->motion()->x = 2.0f;
valuable->motion()->x = 6.0f;
close->durability()->state.health = close->durability()->state.maxHealth = 100.0;
valuable->durability()->state.health = valuable->durability()->state.maxHealth = 100.0;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "priority-rifle";
definition.damage = 1.0f;
definition.range = 10.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;
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 state;
auto step =
eve::SimulationStep{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.1).expect("priority combat step")};
REQUIRE(eve::rts::CombatFireSystem::step(step, state, sensing, damage).ok());
CHECK_EQ(ecs::try_get(attacker->combat()->target), valuable);
REQUIRE(valuable->tags()->values.add("armored").ok());
attacker->combat()->targetPriorities.clear();
attacker->combat()->target = {};
definition.preferredTargetTags = {"armored"};
definition.preferredTargetBonus = 4.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;
step.tick = eve::SimulationTick{2};
REQUIRE(eve::rts::CombatFireSystem::step(step, state, sensing, damage).ok());
CHECK_EQ(ecs::try_get(attacker->combat()->target), valuable);
CommandSpec explicitAttack;
explicitAttack.kind = OrderKind::Attack;
explicitAttack.targetEntity = closeHandle;
REQUIRE(attacker->orders()->values.replace(explicitAttack).ok());
step.tick = eve::SimulationTick{3};
REQUIRE(eve::rts::CombatFireSystem::step(step, state, sensing, damage).ok());
CHECK_EQ(ecs::try_get(attacker->combat()->target), close);
weapon->release();
valuable->release();
close->release();
attacker->release();
red->release();
blue->release();
}
TEST_CASE("rts.weaponTargetDomainAndTagsFilterAutomaticAndExplicitAttacks") {
ecs::Table world;
ecs::ScopedTable guard(world);
auto blueHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002c1")));
auto redHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002c2")));
auto attackerHandle = ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002c3")));
auto groundHandle = ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002c4")));
auto airHandle = ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002c5")));
auto* attacker = dynamic_cast<Unit*>(ecs::try_get(attackerHandle));
auto* ground = dynamic_cast<Unit*>(ecs::try_get(groundHandle));
auto* air = dynamic_cast<Unit*>(ecs::try_get(airHandle));
auto bind = [&](Unit& unit, ecs::EntityHandle faction) {
auto link = eve::rts::FactionLink::bind(faction);
REQUIRE(link.ok());
unit.faction()->link = std::move(link).takeValue();
};
bind(*attacker, blueHandle);
bind(*ground, redHandle);
bind(*air, redHandle);
attacker->combat()->acquisitionRange = 10.0f;
ground->motion()->x = 2.0f;
air->motion()->x = 5.0f;
air->motion()->airborne = true;
REQUIRE(air->tags()->values.add("armored").ok());
ground->durability()->state.health = ground->durability()->state.maxHealth = 100.0;
air->durability()->state.health = air->durability()->state.maxHealth = 100.0;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "anti-air";
definition.damage = 0.0f;
definition.range = 10.0f;
definition.targetsGround = false;
definition.targetsAir = true;
definition.requiredTargetTags = {"armored"};
definition.excludedTargetTags = {"cloaked"};
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;
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 state;
auto step =
eve::SimulationStep{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.1).expect("target filter step")};
REQUIRE(eve::rts::CombatFireSystem::step(step, state, sensing, damage).ok());
CHECK_EQ(ecs::try_get(attacker->combat()->target), air);
CommandSpec explicitGround;
explicitGround.kind = OrderKind::Attack;
explicitGround.targetEntity = groundHandle;
REQUIRE(attacker->orders()->values.replace(explicitGround).ok());
step.tick = eve::SimulationTick{2};
REQUIRE(eve::rts::CombatFireSystem::step(step, state, sensing, damage).ok());
CHECK_EQ(ecs::try_get(attacker->combat()->target), nullptr);
CHECK(attacker->orders()->values.empty());
REQUIRE(air->tags()->values.add("cloaked").ok());
step.tick = eve::SimulationTick{3};
REQUIRE(eve::rts::CombatFireSystem::step(step, state, sensing, damage).ok());
CHECK_EQ(ecs::try_get(attacker->combat()->target), nullptr);
weapon->release();
dynamic_cast<Unit*>(ecs::try_get(airHandle))->release();
dynamic_cast<Unit*>(ecs::try_get(groundHandle))->release();
attacker->release();
dynamic_cast<Faction*>(ecs::try_get(redHandle))->release();
dynamic_cast<Faction*>(ecs::try_get(blueHandle))->release();
}
TEST_CASE("rts.weaponRangeFalloffScalesHitscanDamageAtConfiguredRange") {
ecs::Table world;
ecs::ScopedTable guard(world);
auto blueHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002d1")));
auto redHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002d2")));
auto attackerHandle = ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002d3")));
auto targetHandle = ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002d4")));
auto* attacker = dynamic_cast<Unit*>(ecs::try_get(attackerHandle));
auto* target = dynamic_cast<Unit*>(ecs::try_get(targetHandle));
auto blueLink = eve::rts::FactionLink::bind(blueHandle);
auto redLink = eve::rts::FactionLink::bind(redHandle);
REQUIRE(blueLink.ok());
REQUIRE(redLink.ok());
attacker->faction()->link = std::move(blueLink).takeValue();
target->faction()->link = std::move(redLink).takeValue();
attacker->combat()->acquisitionRange = 10.0f;
target->motion()->x = 10.0f;
target->durability()->state.health = target->durability()->state.maxHealth = 100.0;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "falloff-rifle";
definition.damage = 20.0f;
definition.range = 10.0f;
definition.falloffStart = 0.0f;
definition.minimumDamageFactor = 0.5f;
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;
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 state;
auto fired = eve::rts::CombatFireSystem::step(
{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.1).expect("falloff shot")}, state, sensing, damage);
REQUIRE(fired.ok());
CHECK_EQ(fired.value(), 1u);
CHECK(std::abs(target->durability()->state.health - 90.0) < 1e-5);
weapon->release();
target->release();
attacker->release();
dynamic_cast<Faction*>(ecs::try_get(redHandle))->release();
dynamic_cast<Faction*>(ecs::try_get(blueHandle))->release();
}
TEST_CASE("rts.combatWaitsForTurretToTraverseBeforeFiring") {
ecs::Table world;
ecs::ScopedTable guard(world);
auto blueHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002b1")));
auto redHandle = ecs::handle_of(Faction::createFaction(subject("00000000-0000-7000-8000-0000000002b2")));
auto attackerHandle = ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002b3")));
auto targetHandle = ecs::handle_of(Unit::createUnit(subject("00000000-0000-7000-8000-0000000002b4")));
auto* blue = dynamic_cast<Faction*>(ecs::try_get(blueHandle));
auto* red = dynamic_cast<Faction*>(ecs::try_get(redHandle));
auto* attacker = dynamic_cast<Unit*>(ecs::try_get(attackerHandle));
auto* target = dynamic_cast<Unit*>(ecs::try_get(targetHandle));
auto blueLink = eve::rts::FactionLink::bind(blueHandle);
auto redLink = eve::rts::FactionLink::bind(redHandle);
REQUIRE(blueLink.ok());
REQUIRE(redLink.ok());
attacker->faction()->link = std::move(blueLink).takeValue();
target->faction()->link = std::move(redLink).takeValue();
attacker->combat()->acquisitionRange = 10.0f;
attacker->combat()->turnRateDegrees = 90.0f;
attacker->combat()->aimToleranceDegrees = 1.0f;
target->motion()->x = -5.0f;
target->durability()->state.health = target->durability()->state.maxHealth = 100.0;
auto* weapon = eve::weapon::WeaponEntity::createWeapon();
eve::weapon::WeaponDefinition definition;
definition.id = "traversing-rifle";
definition.damage = 10.0f;
definition.range = 10.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;
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 state;
auto step =
eve::SimulationStep{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("turret traversal step")};
auto first = eve::rts::CombatFireSystem::step(step, state, sensing, damage);
REQUIRE(first.ok());
CHECK_EQ(first.value(), 0u);
CHECK(std::abs(std::abs(weapon->aim()->yaw) - 90.0f) < 1e-5f);
CHECK(std::abs(target->durability()->state.health - 100.0) < 1e-5);
step.tick = eve::SimulationTick{2};
auto second = eve::rts::CombatFireSystem::step(step, state, sensing, damage);
REQUIRE(second.ok());
CHECK_EQ(second.value(), 1u);
CHECK(std::abs(target->durability()->state.health - 90.0) < 1e-5);
weapon->release();
target->release();
attacker->release();
red->release();
blue->release();
}
TEST_CASE("rts.facadeHealsCombatRootsAndAppliesCanonicalStatusDefinitions") {
ecs::Table world;
ecs::ScopedTable guard(world);
eve::rts::RTS rts;
eve::definitions::DefinitionRegistry definitions;
const std::string content = R"json({
"statusEffects":[{
"id":"field_repair","duration":6,"speedMultiplier":0.8,
"damageMultiplier":1.1,"incomingDamageMultiplier":0.9,"healingPerSecond":3
}]
})json";
REQUIRE(rts.loadContent(definitions, content).ok());
const auto sourceSubject = subject("00000000-0000-7000-8000-0000000002f1");
const auto targetSubject = subject("00000000-0000-7000-8000-0000000002f2");
const auto buildingSubject = subject("00000000-0000-7000-8000-0000000002f3");
auto sourceCreated = rts.newUnit(sourceSubject);
auto targetCreated = rts.newUnit(targetSubject);
auto buildingCreated = rts.newBuilding(buildingSubject);
REQUIRE(sourceCreated.ok());
REQUIRE(targetCreated.ok());
REQUIRE(buildingCreated.ok());
auto* target = targetCreated.value();
auto* building = buildingCreated.value();
target->durability()->state.health = 35.0;
target->durability()->state.maxHealth = 50.0;
building->integrity()->state.health = 70.0;
building->integrity()->state.maxHealth = 100.0;
auto unitHealing = rts.heal(sourceSubject, targetSubject, 100.0);
auto buildingHealing = rts.heal(sourceSubject, buildingSubject, 12.0);
REQUIRE(unitHealing.ok());
REQUIRE(buildingHealing.ok());
CHECK_EQ(unitHealing.value(), 15.0);
CHECK_EQ(buildingHealing.value(), 12.0);
CHECK_EQ(target->durability()->state.health, 50.0);
CHECK_EQ(building->integrity()->state.health, 82.0);
CHECK(!rts.heal(sourceSubject, targetSubject, -1.0).ok());
CHECK_EQ(target->durability()->state.health, 50.0);
auto applied = rts.applyStatusEffect(sourceSubject, targetSubject, "field_repair", 2.5);
REQUIRE(applied.ok());
CHECK(applied.value().isValid());
CHECK_EQ(target->effects()->values.count(), 1u);
CHECK(std::abs(target->effects()->values.multiplier("speedMultiplier") - 0.8) < 1e-6);
CHECK(std::abs(target->effects()->values.additive("healingPerSecond") - 3.0) < 1e-6);
auto missing = rts.applyStatusEffect(sourceSubject, targetSubject, "missing", -1.0);
CHECK(!missing.ok());
CHECK_EQ(target->effects()->values.count(), 1u);
auto snapshot = rts.snapshotState();
REQUIRE(snapshot.ok());
CHECK_EQ(snapshot.value().units.back().effects.effects.effectCount(), 1);
auto inspected = rts.inspectState().toJson();
REQUIRE(inspected.ok());
CHECK(inspected.value().find("\"activeEffects\":1") != std::string::npos);
}