#include "testHarness.h" #include "vf/Raid.h" using namespace vf; namespace { constexpr int groundTop = 4; constexpr int walkY = groundTop + 1; const Vec3i villageCenter{0, walkY, 0}; World makeRaidWorld() { World world; for (int x = -28; x <= 28; ++x) for (int z = -28; z <= 28; ++z) for (int y = 0; y <= groundTop; ++y) world.setBlock(x, y, z, Block::Stone); world.setBlock(0, walkY, 0, Block::Bell); world.setBlock(4, walkY, 0, Block::Bed); world.setBlock(4, walkY, 2, Block::Bed); world.setBlock(4, walkY, 4, Block::Bed); world.setBlock(4, walkY, 6, Block::Bed); world.setBlock(-4, walkY, 0, Block::Lectern); world.setBlock(-4, walkY, 2, Block::Composter); return world; } std::vector makeVillagers(int count) { std::vector villagers; for (int i = 0; i < count; ++i) { Villager villager = makeVillager(i + 1, {static_cast(i) + 0.5f, static_cast(walkY), 1.5f}, 100u + static_cast(i)); villager.profession = VillagerProfession::Unemployed; villagers.push_back(villager); } return villagers; } OmenState omenAt(int level) { OmenState omen; applyBadOmen(omen, level); return omen; } float distanceToCentre(const Vec3& position) { float dx = position.x - (static_cast(villageCenter.x) + 0.5f); float dz = position.z - (static_cast(villageCenter.z) + 0.5f); return std::sqrt(dx * dx + dz * dz); } int livingVillagerCount(const std::vector& villagers) { int count = 0; for (const Villager& villager : villagers) if (villager.alive) ++count; return count; } } // namespace VF_TEST(badOmenExpiresOnItsOwnAndCannotArmARaidAfterwards) { OmenState omen = omenAt(2); CHECK(omen.hasBadOmen()); CHECK_EQ(omen.level, 2); CHECK_EQ(omen.ticksRemaining, badOmenDurationTicks); applyBadOmen(omen, 1); CHECK_EQ(omen.level, 2); applyBadOmen(omen, 4); CHECK_EQ(omen.level, 4); applyBadOmen(omen, badOmenMaxLevel + 3); CHECK_EQ(omen.level, badOmenMaxLevel); tickOmen(omen, badOmenDurationTicks - 1); CHECK(omen.hasBadOmen()); tickOmen(omen, 5); CHECK(!omen.hasBadOmen()); CHECK_EQ(omen.level, 0); RaidManager raid(villageCenter, 1u); CHECK(!raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); CHECK(raid.state() == RaidState::Inactive); } VF_TEST(aRaidStartsOnlyForAnOmenBearingPlayerInsideTheVillage) { RaidManager raid(villageCenter, 7u); OmenState clean; CHECK(!raid.tryStart(clean, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); OmenState omen = omenAt(1); CHECK(!raid.tryStart(omen, {200.5f, static_cast(walkY), 0.5f}, 32.0f)); CHECK(raid.state() == RaidState::Inactive); CHECK(omen.hasBadOmen()); CHECK(raid.tryStart(omen, {6.5f, static_cast(walkY), 3.5f}, 32.0f)); CHECK(raid.state() == RaidState::Spawning); CHECK(raid.active()); CHECK(raid.villageThreatened()); CHECK_EQ(raid.badOmenLevel(), 1); CHECK(!omen.hasBadOmen()); CHECK_EQ(raid.waveCount(), raidWaveCount(raidDifficultyNormal)); CHECK_EQ(raid.waveIndex(), 0); OmenState second = omenAt(1); CHECK(!raid.tryStart(second, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); CHECK(second.hasBadOmen()); } VF_TEST(wavesGrowWithTheirIndexTheDifficultyAndTheOmenLevel) { int previous = 0; for (int index = 0; index < 7; ++index) { RaidWave plan = planRaidWave(index, 1, raidDifficultyNormal); CHECK_EQ(plan.index, index); CHECK(plan.total() > previous); previous = plan.total(); CHECK_EQ(plan.countOf(RaiderKind::Raider), plan.raiders); CHECK_EQ(plan.countOf(RaiderKind::Ravager), plan.ravagers); } CHECK_EQ(planRaidWave(0, 1, raidDifficultyNormal).vindicators, 0); CHECK(planRaidWave(1, 1, raidDifficultyNormal).vindicators > 0); CHECK_EQ(planRaidWave(1, 1, raidDifficultyNormal).witches, 0); CHECK(planRaidWave(2, 1, raidDifficultyNormal).witches > 0); CHECK_EQ(planRaidWave(2, 1, raidDifficultyNormal).ravagers, 0); CHECK(planRaidWave(3, 1, raidDifficultyNormal).ravagers > 0); CHECK(planRaidWave(3, 1, raidDifficultyHard).total() > planRaidWave(3, 1, raidDifficultyEasy).total()); CHECK(planRaidWave(3, 5, raidDifficultyNormal).total() > planRaidWave(3, 1, raidDifficultyNormal).total()); CHECK_EQ(raidWaveCount(raidDifficultyEasy), 3); CHECK_EQ(raidWaveCount(raidDifficultyNormal), 5); CHECK_EQ(raidWaveCount(raidDifficultyHard), 7); } VF_TEST(everySpawnedRaiderStartsOnTheOutskirtsAndWalksTowardsTheBell) { World world = makeRaidWorld(); PoiRegistry pois; pois.scan(world, villageCenter, 12, 4); std::vector villagers = makeVillagers(4); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); OmenState omen = omenAt(1); RaidManager raid(villageCenter, 2024u); CHECK(raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); raid.tick(world, villagers, pois, omen, schedule, clock, 1); CHECK(raid.state() == RaidState::Fighting); CHECK_EQ(static_cast(raid.raiders().size()), raid.wave().total()); CHECK_EQ(raid.livingRaiders(), raid.wave().total()); std::vector startDistances; for (const Raider& attacker : raid.raiders()) { float distance = distanceToCentre(attacker.position); CHECK(distance > raidSpawnRadius * 0.5f); CHECK(!attacker.path.empty()); startDistances.push_back(distance); } raid.tick(world, villagers, pois, omen, schedule, clock, 200); for (std::size_t i = 0; i < raid.raiders().size(); ++i) { if (!raid.raiders()[i].alive) continue; CHECK(distanceToCentre(raid.raiders()[i].position) < startDistances[i]); } } VF_TEST(theRaidRunsThroughEveryWaveAndEndsInVictory) { World world = makeRaidWorld(); PoiRegistry pois; pois.scan(world, villageCenter, 12, 4); std::vector villagers = makeVillagers(4); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); OmenState omen = omenAt(2); RaidManager raid(villageCenter, 555u, raidDifficultyNormal); CHECK(raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); int expectedSpawns = 0; for (int index = 0; index < raid.waveCount(); ++index) expectedSpawns += planRaidWave(index, 2, raidDifficultyNormal).total(); int guard = 0; std::vector wavesSeen; while (raid.active() && guard++ < 20000) { raid.tick(world, villagers, pois, omen, schedule, clock, 1); if (wavesSeen.empty() || wavesSeen.back() != raid.waveIndex()) wavesSeen.push_back(raid.waveIndex()); for (int i = 0; i < static_cast(raid.raiders().size()); ++i) raid.damageRaider(i, 1000.0f); } CHECK(raid.state() == RaidState::Victory); CHECK_EQ(static_cast(wavesSeen.size()), raid.waveCount()); CHECK_EQ(raid.waveIndex(), raid.waveCount() - 1); CHECK_EQ(raid.totalSpawned(), expectedSpawns); CHECK_EQ(raid.livingRaiders(), 0); CHECK_EQ(raid.villagersLost(), 0); CHECK_EQ(livingVillagerCount(villagers), 4); CHECK(!raid.active()); CHECK(raid.elapsedTicks() > raidWaveDelayTicks * (raid.waveCount() - 1)); } VF_TEST(victoryLeavesMajorPositiveGossipAndHeroOfTheVillageWithEverySurvivor) { World world = makeRaidWorld(); PoiRegistry pois; pois.scan(world, villageCenter, 12, 4); std::vector villagers = makeVillagers(4); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); OmenState omen = omenAt(3); RaidManager raid(villageCenter, 909u, raidDifficultyEasy); CHECK(raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); int guard = 0; while (raid.active() && guard++ < 20000) { raid.tick(world, villagers, pois, omen, schedule, clock, 1); for (int i = 0; i < static_cast(raid.raiders().size()); ++i) raid.damageRaider(i, 1000.0f); } CHECK(raid.state() == RaidState::Victory); for (const Villager& villager : villagers) { CHECK_EQ(villager.gossip.valueOf(GossipKind::MajorPositive), gossipMajorPositiveGain); CHECK(villager.gossip.reputation() > 0); } CHECK(omen.isHero()); CHECK_EQ(omen.heroLevel, 3); CHECK_EQ(omen.heroTicksRemaining, heroDurationTicks); CHECK(!omen.hasBadOmen()); tickOmen(omen, heroDurationTicks); CHECK(!omen.isHero()); CHECK_EQ(omen.heroLevel, 0); } VF_TEST(aRaidThatKillsEveryVillagerEndsInDefeat) { World world = makeRaidWorld(); PoiRegistry pois; pois.scan(world, villageCenter, 12, 4); std::vector villagers = makeVillagers(2); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); OmenState omen = omenAt(1); RaidManager raid(villageCenter, 31u, raidDifficultyEasy); CHECK(raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); CHECK_EQ(golemCapFor(2), 0); int guard = 0; while (raid.active() && guard++ < 20000) raid.tick(world, villagers, pois, omen, schedule, clock, 1); CHECK(raid.state() == RaidState::Defeat); CHECK_EQ(livingVillagerCount(villagers), 0); CHECK_EQ(raid.villagersLost(), 2); CHECK_EQ(raid.golems().spawnedTotal, 0); CHECK(!omen.isHero()); for (const Villager& villager : villagers) { CHECK_EQ(villager.gossip.valueOf(GossipKind::MajorPositive), 0); CHECK_EQ(villager.workstationIndex, -1); CHECK_EQ(villager.bedIndex, -1); } CHECK_EQ(pois.freeBedCount(), pois.bedCount()); } VF_TEST(golemsSpawnInProportionToThePopulationAndStopAtTheCap) { CHECK_EQ(golemCapFor(0), 0); CHECK_EQ(golemCapFor(golemMinimumVillagers - 1), 0); CHECK_EQ(golemCapFor(golemMinimumVillagers), 1); CHECK_EQ(golemCapFor(8), 2); CHECK_EQ(golemCapFor(12), 3); GolemSpawner spawner; int spawns = 0; for (int tick = 0; tick < 2000; ++tick) if (tickGolemSpawner(spawner, 12, 1)) ++spawns; CHECK_EQ(spawns, 3); CHECK_EQ(spawner.golemsAlive, 3); CHECK_EQ(spawner.spawnedTotal, 3); spawner.golemsAlive = 0; CHECK(tickGolemSpawner(spawner, 12, spawner.intervalTicks + 1)); CHECK_EQ(spawner.spawnedTotal, 4); GolemSpawner hamlet; for (int tick = 0; tick < 2000; ++tick) tickGolemSpawner(hamlet, 2, 1); CHECK_EQ(hamlet.spawnedTotal, 0); CHECK(!tickGolemSpawner(hamlet, 12, 0)); } VF_TEST(aDefendedVillageFieldsGolemsWhileTheRaidIsRunning) { World world = makeRaidWorld(); PoiRegistry pois; pois.scan(world, villageCenter, 12, 4); std::vector villagers = makeVillagers(4); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); OmenState omen = omenAt(1); RaidManager raid(villageCenter, 77u, raidDifficultyEasy); CHECK(raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); raid.tick(world, villagers, pois, omen, schedule, clock, 400); CHECK(raid.golems().spawnedTotal >= 1); CHECK(raid.golems().golemsAlive >= 1); CHECK(raid.golems().golemsAlive <= golemCapFor(4)); CHECK(raid.livingRaiders() < raid.wave().total()); } VF_TEST(golemsTurnOnThePlayerOnceTheVillageReputationCollapses) { GolemSpawner spawner; GossipLedger calm; calm.add(GossipKind::Trading, gossipTradingGain * 4); updateGolemHostility(spawner, buildReputation(calm, {})); CHECK(!spawner.hostileToPlayer); GossipLedger angry; angry.add(GossipKind::MajorNegative, gossipMajorNegativeGain); std::vector village{&angry, &angry}; updateGolemHostility(spawner, buildReputation(calm, village)); CHECK(spawner.hostileToPlayer); std::vector mixed{&angry, &calm, &calm, &calm, &calm, &calm}; updateGolemHostility(spawner, buildReputation(calm, mixed)); CHECK(!spawner.hostileToPlayer); } VF_TEST(twoRaidManagersOnTheSameSeedProduceTheSameRaid) { World world = makeRaidWorld(); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); PoiRegistry firstPois; firstPois.scan(world, villageCenter, 12, 4); std::vector firstVillagers = makeVillagers(4); OmenState firstOmen = omenAt(2); RaidManager first(villageCenter, 4242u, raidDifficultyHard); CHECK(first.tryStart(firstOmen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); first.tick(world, firstVillagers, firstPois, firstOmen, schedule, clock, 300); PoiRegistry secondPois; secondPois.scan(world, villageCenter, 12, 4); std::vector secondVillagers = makeVillagers(4); OmenState secondOmen = omenAt(2); RaidManager second(villageCenter, 4242u, raidDifficultyHard); CHECK(second.tryStart(secondOmen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); second.tick(world, secondVillagers, secondPois, secondOmen, schedule, clock, 300); CHECK_EQ(first.totalSpawned(), second.totalSpawned()); CHECK_EQ(first.livingRaiders(), second.livingRaiders()); CHECK_EQ(first.golems().spawnedTotal, second.golems().spawnedTotal); CHECK(first.state() == second.state()); bool identical = first.raiders().size() == second.raiders().size(); for (std::size_t i = 0; i < first.raiders().size() && identical; ++i) { const Raider& a = first.raiders()[i]; const Raider& b = second.raiders()[i]; if (a.kind != b.kind || a.alive != b.alive) identical = false; if (std::fabs(a.position.x - b.position.x) > 1e-5f) identical = false; if (std::fabs(a.position.z - b.position.z) > 1e-5f) identical = false; if (std::fabs(a.health - b.health) > 1e-5f) identical = false; } CHECK(identical); RaidManager other(villageCenter, 99u, raidDifficultyHard); OmenState otherOmen = omenAt(2); std::vector otherVillagers = makeVillagers(4); PoiRegistry otherPois; otherPois.scan(world, villageCenter, 12, 4); CHECK(other.tryStart(otherOmen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); other.tick(world, otherVillagers, otherPois, otherOmen, schedule, clock, 1); CHECK_EQ(other.wave().total(), first.wave().total()); } VF_TEST(villagersPanicHomeInsteadOfWorkingWhileTheRaidIsRunning) { World world = makeRaidWorld(); PoiRegistry pois; pois.scan(world, villageCenter, 12, 4); std::vector villagers = makeVillagers(2); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); for (int tick = 0; tick < 400; ++tick) for (Villager& villager : villagers) tickVillager(world, villager, pois, clock, schedule, villageCenter, false, 0.05f); CHECK(villagers[0].activity == VillagerActivity::Work); CHECK(villagers[0].employed()); int workBefore = villagers[0].workTicks; OmenState omen = omenAt(1); RaidManager raid(villageCenter, 8u, raidDifficultyEasy); CHECK(raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); raid.tick(world, villagers, pois, omen, schedule, clock, 120); CHECK(villagers[0].activity == VillagerActivity::Panic); CHECK_EQ(villagers[0].workTicks, workBefore); CHECK(villagers[0].bedIndex >= 0); CHECK(villagers[0].alive); CHECK(pois.at(villagers[0].bedIndex).ownerId == villagers[0].id); } VF_TEST(aWaveAdvancesOnTimeoutWhenTheRaidersRefuseToDie) { World world = makeRaidWorld(); PoiRegistry pois; pois.scan(world, villageCenter, 12, 4); std::vector villagers = makeVillagers(2); VillagerSchedule schedule; TimeOfDay clock(static_cast(0.35 * 24000.0)); OmenState omen = omenAt(1); RaidManager raid(villageCenter, 606u, raidDifficultyNormal); CHECK(raid.tryStart(omen, {0.5f, static_cast(walkY), 0.5f}, 32.0f)); int firstWaveSize = 0; for (int tick = 0; tick < raidWaveTimeoutTicks - 5; ++tick) { raid.tick(world, villagers, pois, omen, schedule, clock, 1); for (Villager& villager : villagers) if (villager.alive) villager.health = 20.0f; if (firstWaveSize == 0) firstWaveSize = raid.totalSpawned(); } CHECK_EQ(raid.waveIndex(), 0); CHECK_EQ(raid.totalSpawned(), firstWaveSize); CHECK(raid.livingRaiders() > 0); for (int tick = 0; tick < 20; ++tick) { raid.tick(world, villagers, pois, omen, schedule, clock, 1); for (Villager& villager : villagers) if (villager.alive) villager.health = 20.0f; } CHECK_EQ(raid.waveIndex(), 1); CHECK(raid.totalSpawned() > firstWaveSize); CHECK(raid.state() == RaidState::Fighting); CHECK_EQ(livingVillagerCount(villagers), 2); } VF_TEST(raiderKindsCarryDistinctStatsAndReadableNames) { const Raider::Stats& footSoldier = raiderStats(RaiderKind::Raider); const Raider::Stats& brute = raiderStats(RaiderKind::Ravager); CHECK(brute.maxHealth > footSoldier.maxHealth); CHECK(brute.damage > footSoldier.damage); CHECK(brute.speed > footSoldier.speed); CHECK(raiderStats(RaiderKind::Witch).attackRange > footSoldier.attackRange); CHECK_EQ(std::string(raiderKindName(RaiderKind::Vindicator)), std::string("vindicator")); CHECK_EQ(std::string(raidStateName(RaidState::Victory)), std::string("victory")); CHECK_EQ(std::string(raidStateName(RaidState::Defeat)), std::string("defeat")); RaidManager raid(villageCenter, 1u); CHECK(raid.center() == villageCenter); CHECK_EQ(raid.livingRaiders(), 0); CHECK(!raid.damageRaider(0, 5.0f)); CHECK(raid.state() == RaidState::Inactive); }