160 lines
5.5 KiB
C++
160 lines
5.5 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;
|
|
|