diff --git a/tests/testBoss.cpp b/tests/testBoss.cpp index bc4e0ca..ea87e75 100644 --- a/tests/testBoss.cpp +++ b/tests/testBoss.cpp @@ -157,3 +157,160 @@ VF_TEST(livingCrystalsHealTheDragonFasterThanTheSwordDrainsIt) { 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); +