diff --git a/tests/testBoss.cpp b/tests/testBoss.cpp index ea87e75..4fca886 100644 --- a/tests/testBoss.cpp +++ b/tests/testBoss.cpp @@ -314,3 +314,163 @@ VF_TEST(theWitherIsCalledOutOfASoulSandPatternInEitherOrientation) { 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}); +