Files
voxelforge/tests/testBoss.cpp
T
2026-09-10 21:12:37 +02:00

317 lines
11 KiB
C++

#include "testHarness.h"
#include "vf/Boss.h"
#include <string>
#include <vector>
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<int>(fight.crystals().size()); ++index) {
if (fight.crystals()[static_cast<std::size_t>(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<WitherTarget>& targets) {
std::vector<WitherHit> 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<int>(BossKind::Count); ++index) {
CHECK(std::string(bossKindName(static_cast<BossKind>(index))) != std::string("unknown"));
}
for (int index = 0; index < static_cast<int>(BossPhase::Count); ++index) {
CHECK(std::string(bossPhaseName(static_cast<BossPhase>(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<ObsidianPillar> pillars = end.pillars();
std::vector<CrystalNode> 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<float>(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<int>(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<int>(fight.crystals().size()));
}
VF_TEST(aCagedCrystalIgnoresShotsUntilTheCageComesDown) {
EndGenerator end;
DragonFight fight(end, 7u);
int cagedIndex = -1;
for (int index = 0; index < static_cast<int>(fight.crystals().size()); ++index) {
if (fight.crystals()[static_cast<std::size_t>(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<std::size_t>(cagedIndex)].alive);
DragonPlayerInput breaking = blocked;
breaking.breakCage = true;
CHECK_EQ(fight.tick(breaking).crystalsDestroyed, 0);
CHECK(!fight.crystals()[static_cast<std::size_t>(cagedIndex)].cageIntact);
CHECK_EQ(fight.tick(blocked).crystalsDestroyed, 1);
CHECK(!fight.crystals()[static_cast<std::size_t>(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<int>(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<float>& 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<float> firstTrack;
std::vector<float> 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);