Files

528 lines
24 KiB
C++

#include "RtsCompositionFixtures.h"
TEST_CASE("rts.facadeCommandsContainmentAndCloakUseOwnedStableRoots") {
ecs::Table world;
ecs::ScopedTable guard(world);
eve::rts::RTS rts;
Faction* faction = rts.newFaction(subject("00000000-0000-7000-8000-00000000ee01")).value();
Unit* transport = rts.newFactionUnit(*faction, subject("00000000-0000-7000-8000-00000000ee02")).value();
Unit* passenger = rts.newFactionUnit(*faction, subject("00000000-0000-7000-8000-00000000ee03")).value();
Building* bunker = rts.newFactionBuilding(*faction, subject("00000000-0000-7000-8000-00000000ee04")).value();
transport->containment()->capacity = 2;
transport->motion()->x = 3.0f;
transport->motion()->y = 4.0f;
passenger->motion()->x = 3.0f;
passenger->motion()->y = 4.0f;
passenger->motion()->arrived = true;
CommandSpec board;
board.kind = OrderKind::BoardTransport;
board.target = {3.0f, 4.0f};
board.targetEntity = ecs::handle_of(transport);
const std::array passengerSelection{passenger->identity()->subject};
REQUIRE(rts.commandUnits(passengerSelection, board).ok());
REQUIRE(eve::rts::ContainmentSystem::step().ok());
CHECK(passenger->containment()->container.resolve() == transport);
auto unloaded = rts.unloadTransport(*transport, {8.0f, 5.0f});
REQUIRE(unloaded.ok());
CHECK_EQ(unloaded.value(), std::size_t{1});
CHECK(!passenger->containment()->container.isBound());
auto bunkerLink = eve::rts::ContainerLink::bind(ecs::handle_of(bunker));
REQUIRE(bunkerLink.ok());
passenger->containment()->container = std::move(bunkerLink).takeValue();
bunker->garrison()->occupants.push_back(ecs::handle_of(passenger));
bunker->capture()->blockedByGarrison = true;
auto evacuated = rts.evacuateBuilding(*bunker, {10.0f, 6.0f});
REQUIRE(evacuated.ok());
CHECK_EQ(evacuated.value(), std::size_t{1});
CHECK(!bunker->capture()->blockedByGarrison);
REQUIRE(rts.setUnitCloaked(*passenger, true).ok());
CHECK(passenger->vision()->cloaked);
Unit* foreign = Unit::createUnit(subject("00000000-0000-7000-8000-00000000ee05"));
CHECK(!rts.setUnitCloaked(*foreign, true).ok());
CHECK(!rts.unloadTransport(*foreign, {}).ok());
foreign->release();
}
TEST_CASE("rts.formationPlannerIsDeterministic") {
const FormationSpec spec{FormationKind::Grid, 10.0f, 2};
auto planned = eve::rts::FormationPlanner::plan(3, {100.0f, 200.0f}, spec);
REQUIRE(planned.ok());
const auto positions = std::move(planned).takeValue();
REQUIRE_EQ(positions.size(), 3u);
CHECK(std::abs(positions[0].x - 95.0f) < 1e-5f);
CHECK(std::abs(positions[0].y - 195.0f) < 1e-5f);
CHECK(std::abs(positions[1].x - 105.0f) < 1e-5f);
CHECK(std::abs(positions[1].y - 195.0f) < 1e-5f);
CHECK(std::abs(positions[2].x - 95.0f) < 1e-5f);
CHECK(std::abs(positions[2].y - 205.0f) < 1e-5f);
}
TEST_CASE("rts.commandFanOutUsesGenericOrdersAndFormation") {
ecs::Table world;
ecs::ScopedTable guard(world);
std::vector<Unit*> units;
std::vector<ecs::EntityHandle> handles;
for (int index = 0; index < 3; ++index) {
Unit* unit = Unit::createUnit();
unit->motion()->speed = 100.0f;
units.push_back(unit);
handles.push_back(ecs::handle_of(unit));
}
const CommandSpec command{OrderKind::Move, {100.0f, 200.0f}, {}, 0, 0.0};
const FormationSpec formation{FormationKind::Grid, 10.0f, 2};
auto receiptResult = eve::rts::CommandFanOutSystem::fanOut(handles, command, formation);
REQUIRE(receiptResult.ok());
const auto receipt = std::move(receiptResult).takeValue();
CHECK_EQ(receipt.requested, 3u);
CHECK_EQ(receipt.accepted, 3u);
CHECK_EQ(receipt.orderIds.size(), 3u);
for (std::size_t index = 0; index < units.size(); ++index) {
auto current = units[index]->orders()->values.current();
REQUIRE(current.ok());
const auto order = std::move(current).takeValue();
CHECK_EQ(static_cast<int>(order.kind), static_cast<int>(OrderKind::Move));
CHECK_EQ(order.formationSlot, static_cast<int>(index));
CHECK_EQ(order.id, receipt.orderIds[index]);
}
const std::size_t beforeRejectedFanOut = units[0]->orders()->values.orderCount();
units[2]->release();
auto staleResult = eve::rts::CommandFanOutSystem::fanOut(handles, command, formation);
CHECK(!staleResult.ok());
staleResult.ignore("expected stale selection in fan-out test");
CHECK_EQ(units[0]->orders()->values.orderCount(), beforeRejectedFanOut);
units[0]->release();
units[1]->release();
}
TEST_CASE("rts.commandFanOutReplacesDirectCommandsAndAppendsQueuedWaypoints") {
ecs::Table world;
ecs::ScopedTable guard(world);
Unit* unit = Unit::createUnit();
std::array<ecs::EntityHandle, 1> selection{ecs::handle_of(unit)};
FormationSpec formation{FormationKind::Line, 1.0f, 0};
CommandSpec oldMove;
oldMove.kind = OrderKind::Move;
oldMove.target = {1.0f, 0.0f};
REQUIRE(unit->orders()->values.enqueue(oldMove).ok());
CommandSpec direct = oldMove;
direct.target = {5.0f, 0.0f};
auto replaced = eve::rts::CommandFanOutSystem::fanOut(selection, direct, formation);
REQUIRE(replaced.ok());
CHECK_EQ(unit->orders()->values.orderCount(), 1u);
auto current = unit->orders()->values.current();
REQUIRE(current.ok());
CHECK(std::abs(current.value().target.x - 5.0f) < 1e-5f);
CommandSpec queued = direct;
queued.target = {9.0f, 0.0f};
queued.append = true;
auto appended = eve::rts::CommandFanOutSystem::fanOut(selection, queued, formation);
REQUIRE(appended.ok());
CHECK_EQ(unit->orders()->values.orderCount(), 2u);
current = unit->orders()->values.current();
REQUIRE(current.ok());
CHECK(std::abs(current.value().target.x - 5.0f) < 1e-5f);
unit->release();
}
TEST_CASE("rts.extendedOrdersPreserveEntityAndAreaPayload") {
ecs::Table world;
ecs::ScopedTable guard(world);
Unit* unit = Unit::createUnit();
Unit* target = Unit::createUnit();
CommandSpec suppress;
suppress.kind = OrderKind::SuppressArea;
suppress.target = {12.0f, 8.0f};
suppress.secondaryTarget = {20.0f, 14.0f};
suppress.targetEntity = ecs::handle_of(target);
suppress.radius = 6.0f;
suppress.append = true;
auto queued = unit->orders()->values.enqueue(suppress);
REQUIRE(queued.ok());
std::move(queued).takeValue();
auto current = unit->orders()->values.current();
REQUIRE(current.ok());
const auto order = std::move(current).takeValue();
CHECK_EQ(static_cast<int>(order.kind), static_cast<int>(OrderKind::SuppressArea));
CHECK(std::abs(order.secondaryTarget.x - 20.0f) < 1e-5f);
CHECK(std::abs(order.radius - 6.0f) < 1e-5f);
CHECK(order.append);
CHECK(order.targetEntity.id == ecs::handle_of(target).id);
CHECK(std::abs(unit->worker()->cargo) < 1e-5f);
CHECK(!unit->combat()->holdPosition);
unit->release();
target->release();
}
TEST_CASE("rts.crowdProviderOwnsLinkedMovementAndOverlapResolution") {
ecs::Table world;
ecs::ScopedTable guard(world);
eve::crowd::Crowd crowd;
crowd.setResolveOverlaps(true);
crowd.setSeparationRadius(3.0f);
crowd.setSeparationWeight(2.0f);
Unit* first = Unit::createUnit();
Unit* second = Unit::createUnit();
auto firstLink = eve::rts::CrowdLink::bind("rts/unit/first");
auto secondLink = eve::rts::CrowdLink::bind("rts/unit/second");
REQUIRE(firstLink.ok());
REQUIRE(secondLink.ok());
first->crowd()->link = std::move(firstLink).takeValue();
second->crowd()->link = std::move(secondLink).takeValue();
first->crowd()->radius = 0.75f;
second->crowd()->radius = 0.75f;
first->motion()->speed = 4.0f;
second->motion()->speed = 4.0f;
CommandSpec move;
move.kind = OrderKind::Move;
move.target = {10.0f, 0.0f};
auto firstOrder = first->orders()->values.enqueue(move);
auto secondOrder = second->orders()->values.enqueue(move);
REQUIRE(firstOrder.ok());
REQUIRE(secondOrder.ok());
std::move(firstOrder).takeValue();
std::move(secondOrder).takeValue();
const eve::SimulationStep step{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.25).expect("crowd dt")};
auto moved = eve::rts::CrowdMotionSystem::step(step, crowd);
REQUIRE(moved.ok());
CHECK_EQ(moved.value(), 2u);
CHECK(crowd.hasNamedAgent("rts/unit/first"));
CHECK(crowd.hasNamedAgent("rts/unit/second"));
const float dx = first->motion()->x - second->motion()->x;
const float dy = first->motion()->y - second->motion()->y;
CHECK(std::hypot(dx, dy) > 0.0f);
first->release();
second->release();
}
TEST_CASE("rts.crowdProjectsMovementPriorityAndUnlocksExactOverlap") {
ecs::Table world;
ecs::ScopedTable guard(world);
eve::crowd::Crowd crowd;
crowd.setClampToField(false);
crowd.setResolveOverlaps(true);
crowd.setSeparationRadius(3.0f);
crowd.setSeparationWeight(0.0f);
Unit* high = Unit::createUnit(subject("00000000-0000-7000-8000-000000000401"));
Unit* low = Unit::createUnit(subject("00000000-0000-7000-8000-000000000402"));
auto highLink = eve::rts::CrowdLink::bind("rts/priority/high");
auto lowLink = eve::rts::CrowdLink::bind("rts/priority/low");
REQUIRE(highLink.ok());
REQUIRE(lowLink.ok());
high->crowd()->link = std::move(highLink).takeValue();
low->crowd()->link = std::move(lowLink).takeValue();
high->crowd()->radius = low->crowd()->radius = 1.0f;
high->navigation()->movementPriority = 10;
low->navigation()->movementPriority = -10;
CommandSpec move;
move.kind = OrderKind::Move;
move.target = {10.0f, 0.0f};
REQUIRE(high->orders()->values.enqueue(move).ok());
REQUIRE(low->orders()->values.enqueue(move).ok());
const eve::SimulationStep step{eve::SimulationTick{1},
eve::Duration::fromSeconds(0.1).expect("priority avoidance dt")};
REQUIRE(eve::rts::CrowdMotionSystem::step(step, crowd).ok());
const int highAgent = crowd.getNamedAgentIndex("rts/priority/high");
const int lowAgent = crowd.getNamedAgentIndex("rts/priority/low");
CHECK_EQ(crowd.getAgentAvoidancePriority(highAgent), 10);
CHECK_EQ(crowd.getAgentAvoidancePriority(lowAgent), -10);
CHECK(std::hypot(low->motion()->x, low->motion()->y) > std::hypot(high->motion()->x, high->motion()->y));
high->release();
low->release();
}
TEST_CASE("rts.navigationUsesCanonicalMapPathAndReportsUnreachableOnce") {
ecs::Table world;
ecs::ScopedTable guard(world);
eve::map::Pathfinder pathfinder(5, 3);
pathfinder.setDiagonal(false);
pathfinder.setBlocked(1, 1, true);
Unit* unit = Unit::createUnit();
unit->motion()->x = 0.0f;
unit->motion()->y = 1.0f;
unit->motion()->speed = 1.0f;
CommandSpec move;
move.kind = OrderKind::Move;
move.target = {4.0f, 1.0f};
auto queued = unit->orders()->values.enqueue(move);
REQUIRE(queued.ok());
std::move(queued).takeValue();
int unreachableCount = 0;
auto planned = eve::rts::NavigationSystem::step(pathfinder, {}, [&](Unit& reported, const eve::rts::OrderRecord&) {
CHECK(&reported == unit);
++unreachableCount;
});
REQUIRE(planned.ok());
CHECK(!unit->navigation()->unreachable);
REQUIRE(!unit->navigation()->waypoints.empty());
CHECK(std::abs(unit->navigation()->waypoints.front().y - 1.0f) > 0.5f);
const eve::SimulationStep tick{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("navigation dt")};
auto moved = eve::rts::MotionSystem::step(tick);
REQUIRE(moved.ok());
CHECK(std::abs(unit->motion()->y - 1.0f) > 0.5f);
pathfinder.setBlocked(3, 1, true);
pathfinder.setBlocked(4, 0, true);
pathfinder.setBlocked(4, 2, true);
auto blockedQueue = unit->orders()->values.replace(move);
REQUIRE(blockedQueue.ok());
std::move(blockedQueue).takeValue();
auto blocked = eve::rts::NavigationSystem::step(pathfinder, {},
[&](Unit&, const eve::rts::OrderRecord&) { ++unreachableCount; });
REQUIRE(blocked.ok());
CHECK(unit->navigation()->unreachable);
CHECK_EQ(unreachableCount, 1);
auto repeated = eve::rts::NavigationSystem::step(pathfinder, {},
[&](Unit&, const eve::rts::OrderRecord&) { ++unreachableCount; });
REQUIRE(repeated.ok());
CHECK_EQ(unreachableCount, 1);
unit->release();
}
TEST_CASE("rts.trafficReservationsHonorPriorityAndRecoverAtNarrowCells") {
ecs::Table world;
ecs::ScopedTable guard(world);
eve::map::Pathfinder pathfinder(3, 1);
pathfinder.setDiagonal(false);
Unit* low = Unit::createUnit(subject("00000000-0000-7000-8000-000000000202"));
Unit* high = Unit::createUnit(subject("00000000-0000-7000-8000-000000000201"));
low->motion()->x = 0.0f;
high->motion()->x = 2.0f;
low->navigation()->movementPriority = 1;
high->navigation()->movementPriority = 9;
CommandSpec lowMove;
lowMove.kind = OrderKind::Move;
lowMove.target = {2.0f, 0.0f};
CommandSpec highMove = lowMove;
highMove.target = {0.0f, 0.0f};
REQUIRE(low->orders()->values.enqueue(lowMove).ok());
REQUIRE(high->orders()->values.enqueue(highMove).ok());
REQUIRE(eve::rts::NavigationSystem::step(pathfinder, {}).ok());
auto reserved = eve::rts::TrafficReservationSystem::step(pathfinder, {});
REQUIRE(reserved.ok());
CHECK(low->navigation()->trafficWaiting);
CHECK(!high->navigation()->trafficWaiting);
high->motion()->x = 1.0f;
REQUIRE(eve::rts::NavigationSystem::step(pathfinder, {}).ok());
REQUIRE(eve::rts::TrafficReservationSystem::step(pathfinder, {}).ok());
CHECK(!low->navigation()->trafficWaiting);
low->release();
high->release();
}
TEST_CASE("rts.arrivedMovementOrdersAdvanceTheCanonicalMixedQueue") {
ecs::Table world;
ecs::ScopedTable guard(world);
Unit* unit = Unit::createUnit();
unit->motion()->speed = 2.0f;
CommandSpec first;
first.kind = OrderKind::Move;
first.target = {1.0f, 0.0f};
CommandSpec second;
second.kind = OrderKind::AttackMove;
second.target = {3.0f, 0.0f};
CommandSpec third;
third.kind = OrderKind::Move;
third.target = {4.0f, 0.0f};
REQUIRE(unit->orders()->values.enqueue(first).ok());
REQUIRE(unit->orders()->values.enqueue(second).ok());
REQUIRE(unit->orders()->values.enqueue(third).ok());
const eve::SimulationStep tick{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.5).expect("movement queue dt")};
REQUIRE(eve::rts::MotionSystem::step(tick).ok());
CHECK(unit->motion()->arrived);
auto settled = eve::rts::MovementOrderSystem::step();
REQUIRE(settled.ok());
CHECK_EQ(settled.value(), 1u);
auto current = unit->orders()->values.current();
REQUIRE(current.ok());
CHECK_EQ(static_cast<int>(current.value().kind), static_cast<int>(OrderKind::AttackMove));
REQUIRE(eve::rts::MotionSystem::step(tick).ok());
CHECK(!unit->motion()->arrived);
REQUIRE(eve::rts::MotionSystem::step(tick).ok());
CHECK(unit->motion()->arrived);
REQUIRE(eve::rts::MovementOrderSystem::step().ok());
current = unit->orders()->values.current();
REQUIRE(current.ok());
CHECK_EQ(static_cast<int>(current.value().kind), static_cast<int>(OrderKind::Move));
unit->release();
}
TEST_CASE("rts.patrolPersistsAndAlternatesBetweenCommandOriginAndTarget") {
ecs::Table world;
ecs::ScopedTable guard(world);
Unit* unit = Unit::createUnit();
unit->motion()->x = 1.0f;
unit->motion()->speed = 2.0f;
CommandSpec patrol;
patrol.kind = OrderKind::Patrol;
patrol.target = {3.0f, 0.0f};
REQUIRE(unit->orders()->values.enqueue(patrol).ok());
REQUIRE(eve::rts::PatrolSystem::step().ok());
CHECK(unit->navigation()->patrolInitialized);
CHECK(unit->navigation()->patrolTowardTarget);
CHECK(std::abs(unit->navigation()->patrolOrigin.x - 1.0f) < 1e-5f);
const eve::SimulationStep tick{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("patrol dt")};
REQUIRE(eve::rts::MotionSystem::step(tick).ok());
CHECK(std::abs(unit->motion()->x - 3.0f) < 1e-5f);
REQUIRE(eve::rts::PatrolSystem::step().ok());
CHECK(!unit->navigation()->patrolTowardTarget);
REQUIRE(eve::rts::MotionSystem::step(tick).ok());
CHECK(std::abs(unit->motion()->x - 1.0f) < 1e-5f);
CHECK(!unit->orders()->values.empty());
unit->release();
}
TEST_CASE("rts.radarCreatesQuantizedUntargetableContactsAndJammingStopsRefresh") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000106"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000107"));
Unit* radar = Unit::createUnit(subject("00000000-0000-7000-8000-000000000108"));
Unit* target = Unit::createUnit(subject("00000000-0000-7000-8000-000000000109"));
Unit* jammer = Unit::createUnit(subject("00000000-0000-7000-8000-000000000110"));
auto blueLink = eve::rts::FactionLink::bind(ecs::handle_of(blue));
auto targetLink = eve::rts::FactionLink::bind(ecs::handle_of(red));
auto jammerLink = eve::rts::FactionLink::bind(ecs::handle_of(red));
REQUIRE(blueLink.ok());
REQUIRE(targetLink.ok());
REQUIRE(jammerLink.ok());
radar->faction()->link = std::move(blueLink).takeValue();
target->faction()->link = std::move(targetLink).takeValue();
jammer->faction()->link = std::move(jammerLink).takeValue();
radar->vision()->radarRange = 20.0f;
radar->vision()->radarResolution = 2.0f;
target->motion()->x = 5.2f;
target->motion()->y = 3.1f;
jammer->motion()->x = 30.0f;
eve::map::Fov blueFov(40, 40), redFov(40, 40);
eve::rts::FogOfWarSystem::State fog;
const auto provider = [&](Faction& faction) { return &faction == blue ? &blueFov : &redFov; };
const eve::SimulationStep first{eve::SimulationTick{1}, eve::Duration::fromSeconds(0.5).expect("radar dt")};
REQUIRE(eve::rts::FogOfWarSystem::step(first, {}, fog, provider).ok());
const auto* contact = eve::rts::FogOfWarSystem::contact(*blue, target->identity()->subject);
REQUIRE(contact != nullptr);
CHECK_EQ(contact->kind, "radar_unit");
CHECK(!contact->visible);
CHECK(!contact->detected);
CHECK(std::abs(contact->position.x - 5.0f) < 1e-5f);
CHECK(std::abs(contact->position.y - 3.0f) < 1e-5f);
jammer->motion()->x = 5.2f;
jammer->motion()->y = 3.1f;
jammer->vision()->jammingRange = 3.0f;
target->motion()->x = 9.0f;
const eve::SimulationStep second{eve::SimulationTick{2}, eve::Duration::fromSeconds(1.0).expect("jam dt")};
REQUIRE(eve::rts::FogOfWarSystem::step(second, {}, fog, provider).ok());
contact = eve::rts::FogOfWarSystem::contact(*blue, target->identity()->subject);
REQUIRE(contact != nullptr);
CHECK(std::abs(contact->position.x - 5.0f) < 1e-5f);
CHECK(std::abs(contact->ageSeconds - 1.0) < 1e-6);
eve::rts::FogOfWarSystem::clear(fog);
jammer->release();
target->release();
radar->release();
red->release();
blue->release();
}
TEST_CASE("rts.commandNetworkUsesRelaysCapacityPriorityAndHostileJamming") {
ecs::Table world;
ecs::ScopedTable guard(world);
Faction* blue = Faction::createFaction(subject("00000000-0000-7000-8000-000000000221"));
Faction* red = Faction::createFaction(subject("00000000-0000-7000-8000-000000000222"));
Building* hq = Building::createBuilding(subject("00000000-0000-7000-8000-000000000223"));
Unit* relay = Unit::createUnit(subject("00000000-0000-7000-8000-000000000224"));
Unit* high = Unit::createUnit(subject("00000000-0000-7000-8000-000000000225"));
Unit* low = Unit::createUnit(subject("00000000-0000-7000-8000-000000000226"));
Unit* jammer = Unit::createUnit(subject("00000000-0000-7000-8000-000000000227"));
auto bindFaction = [&](auto& entity, Faction& faction) {
auto link = eve::rts::FactionLink::bind(ecs::handle_of(&faction));
REQUIRE(link.ok());
entity.faction()->link = std::move(link).takeValue();
};
bindFaction(*hq, *blue);
bindFaction(*relay, *blue);
bindFaction(*high, *blue);
bindFaction(*low, *blue);
bindFaction(*jammer, *red);
hq->integrity()->state.health = hq->integrity()->state.maxHealth = 100.0;
hq->command()->range = 10.0f;
hq->command()->capacity = 1;
relay->motion()->x = 8.0f;
relay->command()->range = 12.0f;
relay->command()->capacity = 1;
relay->command()->cost = 1;
relay->command()->priority = 100;
relay->command()->relayRequiresUplink = true;
relay->command()->requiresCommand = true;
high->motion()->x = 16.0f;
high->command()->requiresCommand = true;
high->command()->priority = 10;
low->motion()->x = 17.0f;
low->motion()->speed = 2.0f;
low->command()->requiresCommand = true;
low->command()->priority = 1;
low->command()->outOfCommandSpeedFactor = 0.5f;
jammer->motion()->x = 40.0f;
jammer->command()->jammingRange = 5.0f;
auto connected = eve::rts::CommandNetworkSystem::step();
REQUIRE(connected.ok());
CHECK(relay->command()->relayActive);
CHECK(high->command()->inCommand);
CHECK(!low->command()->inCommand);
CHECK_EQ(relay->command()->load, 1);
jammer->motion()->x = 8.0f;
auto jammed = eve::rts::CommandNetworkSystem::step();
REQUIRE(jammed.ok());
CHECK(relay->command()->jammed);
CHECK(!relay->command()->relayActive);
CHECK(!high->command()->inCommand);
CommandSpec move;
move.kind = OrderKind::Move;
move.target = {27.0f, 0.0f};
REQUIRE(low->orders()->values.replace(move).ok());
auto moved = eve::rts::MotionSystem::step(
{eve::SimulationTick{1}, eve::Duration::fromSeconds(1.0).expect("command motion dt")});
REQUIRE(moved.ok());
CHECK(std::abs(low->motion()->x - 18.0f) < 1e-5f);
jammer->release();
low->release();
high->release();
relay->release();
hq->release();
red->release();
blue->release();
}