#include "testHarness.h" #include "vf/Boss.h" #include #include using namespace vf; namespace { DragonPlayerInput clingingTo(const DragonFight& fight, float melee) { DragonPlayerInput input; input.present = true; input.position = fight.state().position; input.meleeDamage = melee; return input; } // Shoots down every crystal in order, breaking a cage first where one stands. int shatterEveryCrystal(DragonFight& fight) { int destroyed = 0; for (int index = 0; index < static_cast(fight.crystals().size()); ++index) { if (fight.crystals()[static_cast(index)].cageIntact) { DragonPlayerInput breaking = clingingTo(fight, 0.0f); breaking.shootCrystal = index; breaking.breakCage = true; fight.tick(breaking); } DragonPlayerInput shot = clingingTo(fight, 0.0f); shot.shootCrystal = index; destroyed += fight.tick(shot).crystalsDestroyed; } return destroyed; } WitherFightConfig quickSummon(int summonTicks) { WitherFightConfig config; config.summonTicks = summonTicks; return config; } // Drives the wither out of the summon charge and into its ranged phase. void runUpToRangedPhase(WitherFight& fight, std::vector& targets) { std::vector nothing; for (int step = 0; step < 200 && fight.state().phase != BossPhase::Ranged; ++step) { fight.tick(nothing, targets); } } World witherAltar(const Vec3i& base, bool alongX) { World world; world.setBlock(base.x, base.y, base.z, witherBaseBlock()); for (int along = -1; along <= 1; ++along) { int ax = alongX ? base.x + along : base.x; int az = alongX ? base.z : base.z + along; world.setBlock(ax, base.y + 1, az, witherBaseBlock()); world.setBlock(ax, base.y + 2, az, witherHeadBlock()); } return world; } } // namespace VF_TEST(everyBossKindAndPhaseHasAName) { for (int index = 0; index < static_cast(BossKind::Count); ++index) { CHECK(std::string(bossKindName(static_cast(index))) != std::string("unknown")); } for (int index = 0; index < static_cast(BossPhase::Count); ++index) { CHECK(std::string(bossPhaseName(static_cast(index))) != std::string("unknown")); } CHECK(std::string(bossPhaseName(BossPhase::Armored)) == std::string("armored")); } VF_TEST(theBossBarTranslatesHealthIntoNotchesAndPercent) { BossBar bar; bar.style = BossBarStyle::Notched10; bar.setFromHealth(200.0f, 200.0f); CHECK_NEAR(bar.progress, 1.0f, 1e-6f); CHECK_EQ(bar.notchCount(), 10); CHECK_EQ(bar.filledNotches(), 10); CHECK_EQ(bar.percent(), 100); bar.setFromHealth(95.0f, 200.0f); CHECK_EQ(bar.filledNotches(), 4); CHECK_EQ(bar.percent(), 48); bar.setFromHealth(0.0f, 200.0f); CHECK_EQ(bar.filledNotches(), 0); CHECK_EQ(bar.percent(), 0); // Overheal and negative health both clamp instead of drawing past the ends. bar.setFromHealth(500.0f, 200.0f); CHECK_NEAR(bar.progress, 1.0f, 1e-6f); bar.setFromHealth(-10.0f, 200.0f); CHECK_NEAR(bar.progress, 0.0f, 1e-6f); bar.style = BossBarStyle::Notched6; bar.setFromHealth(150.0f, 300.0f); CHECK_EQ(bar.notchCount(), 6); CHECK_EQ(bar.filledNotches(), 3); bar.style = BossBarStyle::Solid; CHECK_EQ(bar.notchCount(), 1); bar.style = BossBarStyle::Notched12; CHECK_EQ(bar.notchCount(), 12); bar.style = BossBarStyle::Notched20; CHECK_EQ(bar.notchCount(), 20); } VF_TEST(crystalsAreBuiltOnTopOfTheGeneratedObsidianPillars) { EndGenerator end; std::vector pillars = end.pillars(); std::vector crystals = crystalsFromPillars(pillars); CHECK_EQ(crystals.size(), pillars.size()); CHECK(crystals.size() >= 8u); int caged = 0; for (std::size_t i = 0; i < crystals.size(); ++i) { CHECK(crystals[i].alive); CHECK_EQ(crystals[i].position.x, pillars[i].base.x); CHECK_EQ(crystals[i].position.z, pillars[i].base.z); CHECK_EQ(crystals[i].position.y, pillars[i].base.y + pillars[i].height + 1); CHECK(crystals[i].caged == pillars[i].crowned); CHECK(crystals[i].cageIntact == pillars[i].crowned); if (crystals[i].caged) ++caged; } // The tall pillars in the generated ring carry cages. CHECK(caged > 0); } VF_TEST(theDragonOrbitsTheGeneratedExitPortalColumn) { EndGenerator end; DragonFight fight(end, 1234u); CHECK(fight.state().kind == BossKind::EnderDragon); CHECK(fight.state().bar.visible); CHECK(fight.state().bar.color == BossBarColor::Pink); CHECK_NEAR(fight.portalCenter().y, static_cast(end.surfaceHeight(0, 0)), 1e-4f); CHECK_EQ(fight.eggPosition().y, end.surfaceHeight(0, 0) + 1); CHECK(fight.brain().structureValid()); DragonPlayerInput idle; idle.present = false; float startAngle = fight.state().orbitAngle; for (int step = 0; step < 20; ++step) fight.tick(idle); CHECK(fight.state().phase == BossPhase::Circling); CHECK(fight.state().orbitAngle > startAngle); float radius = behaviorHorizontalDistance(fight.state().position, fight.portalCenter()); CHECK_NEAR(radius, fight.settings().orbitRadius, 0.05f); CHECK_EQ(fight.aliveCrystalCount(), static_cast(fight.crystals().size())); } VF_TEST(livingCrystalsHealTheDragonFasterThanTheSwordDrainsIt) { EndGenerator end; DragonFight fight(end, 99u); float fullHealth = fight.state().health; float totalHealed = 0.0f; for (int step = 0; step < 200; ++step) { BossTickReport report = fight.tick(clingingTo(fight, 5.0f)); totalHealed += report.healedThisTick; } CHECK(totalHealed > 0.0f); CHECK(fight.state().damageTaken > 0.0f); CHECK_NEAR(fight.state().health, fullHealth, 1e-3f); CHECK(!fight.exitPortalOpen()); CHECK(fight.aliveCrystalCount() == static_cast(fight.crystals().size())); } VF_TEST(aCagedCrystalIgnoresShotsUntilTheCageComesDown) { EndGenerator end; DragonFight fight(end, 7u); int cagedIndex = -1; for (int index = 0; index < static_cast(fight.crystals().size()); ++index) { if (fight.crystals()[static_cast(index)].cageIntact) { cagedIndex = index; break; } } CHECK(cagedIndex >= 0); if (cagedIndex < 0) return; DragonPlayerInput blocked = clingingTo(fight, 0.0f); blocked.shootCrystal = cagedIndex; CHECK_EQ(fight.tick(blocked).crystalsDestroyed, 0); CHECK(fight.crystals()[static_cast(cagedIndex)].alive); DragonPlayerInput breaking = blocked; breaking.breakCage = true; CHECK_EQ(fight.tick(breaking).crystalsDestroyed, 0); CHECK(!fight.crystals()[static_cast(cagedIndex)].cageIntact); CHECK_EQ(fight.tick(blocked).crystalsDestroyed, 1); CHECK(!fight.crystals()[static_cast(cagedIndex)].alive); // A second shot at the same wreck changes nothing. CHECK_EQ(fight.tick(blocked).crystalsDestroyed, 0); // Nonsense indices are ignored rather than crashing. DragonPlayerInput bogus = clingingTo(fight, 0.0f); bogus.shootCrystal = 9999; CHECK_EQ(fight.tick(bogus).crystalsDestroyed, 0); } VF_TEST(theWholeDragonFightRunsFromTheCrystalsToTheEgg) { EndGenerator end; DragonFight fight(end, 4242u); int crystalCount = static_cast(fight.crystals().size()); CHECK_EQ(shatterEveryCrystal(fight), crystalCount); CHECK_EQ(fight.aliveCrystalCount(), 0); int descentTicks = 0; while (fight.state().phase != BossPhase::Perching && descentTicks < 200) { fight.tick(clingingTo(fight, 0.0f)); ++descentTicks; } CHECK(fight.state().phase == BossPhase::Perching); int settleTicks = 0; while (fight.state().position.y > fight.portalCenter().y + fight.settings().perchHeight + 0.01f && settleTicks < 300) { fight.tick(clingingTo(fight, 0.0f)); ++settleTicks; } CHECK(settleTicks < 300); CHECK_NEAR(fight.state().position.y, fight.portalCenter().y + fight.settings().perchHeight, 1e-3f); BossTickReport report; int killTicks = 0; while (!report.defeated && killTicks < 600) { report = fight.tick(clingingTo(fight, 8.0f)); ++killTicks; } CHECK(report.defeated); CHECK(fight.state().phase == BossPhase::Defeated); CHECK(!fight.state().alive); CHECK(!fight.state().bar.visible); CHECK_NEAR(fight.state().health, 0.0f, 1e-4f); CHECK(fight.state().attacksLaunched > 0); CHECK(fight.exitPortalOpen()); CHECK(fight.dragonEggDropped()); CHECK(fight.eggPosition() == Vec3i(0, end.surfaceHeight(0, 0) + 1, 0)); // The tick after the kill is a no-op, not a second death. BossTickReport after = fight.tick(clingingTo(fight, 8.0f)); CHECK(after.defeated); CHECK(!after.phaseChanged); World world; CHECK(fight.placeVictoryStructures(world, end)); CHECK(world.blockAt(0, end.surfaceHeight(0, 0), 0) == Block::EndPortal); CHECK(world.blockAt(2, end.surfaceHeight(0, 0), 2) == Block::Bedrock); } VF_TEST(aLivingDragonRefusesToHandOverItsRewards) { EndGenerator end; DragonFight fight(end, 5u); World world; CHECK(!fight.placeVictoryStructures(world, end)); CHECK_EQ(world.chunkCount(), 0u); CHECK(!fight.dragonEggDropped()); } VF_TEST(twoDragonFightsOnTheSameSeedAndScriptRunIdentically) { EndGenerator end; auto playOut = [&end](std::vector& healthTrack, Vec3& endPosition, int& attacks) { DragonFight fight(end, 31337u); shatterEveryCrystal(fight); for (int step = 0; step < 300; ++step) { BossTickReport report = fight.tick(clingingTo(fight, 3.0f)); healthTrack.push_back(report.bossHealth); } endPosition = fight.state().position; attacks = fight.state().attacksLaunched; }; std::vector firstTrack; std::vector secondTrack; Vec3 firstEnd; Vec3 secondEnd; int firstAttacks = 0; int secondAttacks = 0; playOut(firstTrack, firstEnd, firstAttacks); playOut(secondTrack, secondEnd, secondAttacks); CHECK_EQ(firstTrack.size(), secondTrack.size()); for (std::size_t i = 0; i < firstTrack.size() && i < secondTrack.size(); ++i) { CHECK_NEAR(firstTrack[i], secondTrack[i], 1e-6f); } CHECK_NEAR(firstEnd.x, secondEnd.x, 1e-6f); CHECK_NEAR(firstEnd.y, secondEnd.y, 1e-6f); CHECK_EQ(firstAttacks, secondAttacks); CHECK(firstAttacks > 0); } VF_TEST(theWitherIsCalledOutOfASoulSandPatternInEitherOrientation) { Vec3i base{4, 10, -3}; World alongX = witherAltar(base, true); bool orientation = false; CHECK(witherPatternAt(alongX, base, true)); CHECK(!witherPatternAt(alongX, base, false)); CHECK(findWitherPattern(alongX, base, orientation)); CHECK(orientation); World alongZ = witherAltar(base, false); CHECK(findWitherPattern(alongZ, base, orientation)); CHECK(!orientation); WitherFight fight(11u); CHECK(fight.state().phase == BossPhase::Dormant); CHECK(!fight.state().bar.visible); CHECK(fight.summonFromWorld(alongX, base)); CHECK(fight.state().phase == BossPhase::Summoning); CHECK(fight.state().bar.visible); CHECK(fight.state().bar.style == BossBarStyle::Notched6); CHECK_NEAR(fight.state().position.y, static_cast(base.y + 1), 1e-6f); // The pattern is consumed, so the same altar cannot be reused. CHECK(alongX.blockAt(base.x, base.y, base.z) == Block::Air); CHECK(alongX.blockAt(base.x, base.y + 2, base.z) == Block::Air); WitherFight second(12u); CHECK(!second.summonFromWorld(alongX, base)); CHECK(second.state().phase == BossPhase::Dormant); World broken = witherAltar(base, true); broken.setBlock(base.x, base.y + 2, base.z, Block::Stone); CHECK(!findWitherPattern(broken, base, orientation)); } VF_TEST(theWitherIsUntouchableWhileItChargesAndBlastsOnArrival) { WitherFight fight(5u, quickSummon(6)); fight.begin(Vec3{0.0f, 64.0f, 0.0f}); std::vector targets; targets.push_back(WitherTarget{Vec3{4.0f, 64.0f, 0.0f}, 20.0f, true}); targets.push_back(WitherTarget{Vec3{40.0f, 64.0f, 0.0f}, 20.0f, true}); std::vector heavy; heavy.push_back(WitherHit{100.0f, false}); for (int step = 0; step < 5; ++step) { BossTickReport report = fight.tick(heavy, targets); CHECK(report.phase == BossPhase::Summoning); CHECK_NEAR(report.bossHealth, fight.settings().maxHealth, 1e-4f); } BossTickReport blast = fight.tick(heavy, targets); CHECK(blast.phase == BossPhase::Explosive); CHECK(blast.phaseChanged); CHECK_NEAR(blast.damageToTargets, fight.settings().summonBlastDamage, 1e-4f); CHECK_NEAR(targets[0].health, 20.0f - fight.settings().summonBlastDamage, 1e-4f); CHECK_NEAR(targets[1].health, 20.0f, 1e-4f); BossTickReport settle = fight.tick(std::vector{}, targets); CHECK(settle.phase == BossPhase::Ranged); } VF_TEST(theWitherArmoursUpExactlyAtHalfHealth) { WitherFight fight(21u, quickSummon(3)); fight.begin(Vec3{0.0f, 64.0f, 0.0f}); std::vector targets; targets.push_back(WitherTarget{Vec3{10.0f, 64.0f, 0.0f}, 200.0f, true}); runUpToRangedPhase(fight, targets); CHECK(fight.state().phase == BossPhase::Ranged); CHECK(!fight.armored()); float maxHealth = fight.settings().maxHealth; std::vector nearlyHalf; nearlyHalf.push_back(WitherHit{maxHealth * 0.5f - 1.0f, false}); BossTickReport stillBare = fight.tick(nearlyHalf, targets); CHECK(stillBare.phase == BossPhase::Ranged); CHECK_NEAR(stillBare.bossHealth, maxHealth * 0.5f + 1.0f, 1e-4f); CHECK(!fight.armored()); std::vector theLastPoint; theLastPoint.push_back(WitherHit{1.0f, false}); BossTickReport onTheLine = fight.tick(theLastPoint, targets); CHECK_NEAR(onTheLine.bossHealth, maxHealth * 0.5f, 1e-4f); CHECK(onTheLine.phase == BossPhase::Armored); CHECK(onTheLine.phaseChanged); CHECK(fight.armored()); } VF_TEST(armourStopsProjectilesButNotABladeAndTheWitherRegenerates) { WitherFight fight(33u, quickSummon(3)); fight.begin(Vec3{0.0f, 64.0f, 0.0f}); std::vector targets; targets.push_back(WitherTarget{Vec3{10.0f, 64.0f, 0.0f}, 400.0f, true}); runUpToRangedPhase(fight, targets); std::vector half; half.push_back(WitherHit{fight.settings().maxHealth * 0.5f, false}); fight.tick(half, targets); CHECK(fight.armored()); float before = fight.state().health; std::vector arrows; arrows.push_back(WitherHit{60.0f, true}); BossTickReport shrugged = fight.tick(arrows, targets); CHECK(shrugged.bossHealth >= before); CHECK(shrugged.healedThisTick > 0.0f); before = fight.state().health; std::vector blade; blade.push_back(WitherHit{60.0f, false}); BossTickReport cut = fight.tick(blade, targets); CHECK(cut.bossHealth < before - 50.0f); CHECK(fight.armored()); } VF_TEST(theArmouredWitherCallsMinionsUpToItsLimitAndKeepsShooting) { WitherFight fight(44u, quickSummon(3)); fight.begin(Vec3{0.0f, 64.0f, 0.0f}); std::vector targets; targets.push_back(WitherTarget{Vec3{12.0f, 64.0f, 0.0f}, 100000.0f, true}); runUpToRangedPhase(fight, targets); std::vector half; half.push_back(WitherHit{fight.settings().maxHealth * 0.5f, false}); fight.tick(half, targets); CHECK(fight.armored()); std::vector nothing; float targetHealthBefore = targets[0].health; for (int step = 0; step < 600; ++step) fight.tick(nothing, targets); CHECK_EQ(fight.aliveMinionCount(), fight.settings().maxMinions); CHECK_EQ(fight.state().minionsSummoned, fight.settings().maxMinions); CHECK(targets[0].health < targetHealthBefore); CHECK(fight.state().attacksLaunched > 10); for (const BossMinion& minion : fight.minions()) { CHECK(minion.alive); CHECK(behaviorHorizontalDistance(minion.position, fight.state().position) > 0.0f); } } VF_TEST(theWitherAlwaysSwitchesToTheNearestLivingTarget) { WitherFight fight(55u, quickSummon(3)); fight.begin(Vec3{0.0f, 64.0f, 0.0f}); std::vector targets; targets.push_back(WitherTarget{Vec3{60.0f, 64.0f, 0.0f}, 500.0f, true}); targets.push_back(WitherTarget{Vec3{12.0f, 64.0f, 0.0f}, 500.0f, true}); targets.push_back(WitherTarget{Vec3{90.0f, 64.0f, 0.0f}, 500.0f, true}); runUpToRangedPhase(fight, targets); std::vector nothing; fight.tick(nothing, targets); CHECK_EQ(fight.state().targetIndex, 1); targets[1].alive = false; fight.tick(nothing, targets); CHECK_EQ(fight.state().targetIndex, 0); targets[0].alive = false; targets[2].alive = false; BossTickReport alone = fight.tick(nothing, targets); CHECK_EQ(fight.state().targetIndex, -1); CHECK_NEAR(alone.damageToTargets, 0.0f, 1e-6f); } VF_TEST(theWitherDiesWhenItsHealthRunsOutAndStopsTicking) { WitherFight fight(66u, quickSummon(3)); fight.begin(Vec3{0.0f, 64.0f, 0.0f});