diff --git a/tests/testBoss.cpp b/tests/testBoss.cpp new file mode 100644 index 0000000..bc4e0ca --- /dev/null +++ b/tests/testBoss.cpp @@ -0,0 +1,159 @@ +#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; +