diff --git a/tests/testPotionCatalog.cpp b/tests/testPotionCatalog.cpp new file mode 100644 index 0000000..1b6de3d --- /dev/null +++ b/tests/testPotionCatalog.cpp @@ -0,0 +1,157 @@ +#include +#include + +#include "testHarness.h" +#include "vf/PotionCatalog.h" + +using namespace vf; + +namespace { + +using Ingredient = BrewRecipe::Ingredient; + +PotionItem bottleOf(PotionKind kind, bool upgraded = false, bool extended = false, + PotionItem::Form form = PotionItem::Form::Drinkable) { + PotionItem potion; + potion.kind = kind; + potion.upgraded = upgraded; + potion.extended = extended; + potion.form = form; + return potion; +} + +// Runs a whole ingredient list through an actual brewing stand, one full +// four hundred tick brew per step. +PotionItem replayOnAStand(const std::vector& steps) { + BrewingStandState stand; + setBottle(stand, 0, PotionItem{}); + addFuel(stand, blazePowderForBrews(static_cast(steps.size())) + 1); + for (Ingredient ingredient : steps) { + setIngredient(stand, ingredient, 1); + for (int tick = 0; tick < brewDurationTicks; ++tick) tickBrewing(stand); + } + return stand.bottles[0].potion; +} + +PotionItem replayThroughSteps(const std::vector& steps) { + PotionItem bottle; + for (Ingredient ingredient : steps) { + bool changed = false; + bottle = catalogBrewStep(bottle, ingredient, changed); + if (!changed) return PotionItem{}; + } + return bottle; +} + +int splashDurationThroughTheStand(const PotionItem& potion) { + PotionItem splash = potion; + splash.form = PotionItem::Form::Splash; + EffectSet target; + if (!splashPotion(target, splash, 0.0f, 4.0f)) return 0; + return target.active[0].durationTicks; +} + +} // namespace + +VF_TEST(theCatalogsPublishedConstantsAreTheOnesTheNumbersWereDerivedFrom) { + CHECK_NEAR(splashDurationScale, 0.75f, 0.0001f); + CHECK_NEAR(lingeringDoseScale, 0.25f, 0.0001f); + CHECK_EQ(lingeringCloudTicks, 600); + CHECK_EQ(lingeringCloudTicks, 30 * ticksPerSecond); + + // Pinned against literals rather than the constants, so shifting a constant + // cannot quietly shift the expectations with it. + const PotionCatalog& catalog = potionCatalog(); + PotionItem swiftness = bottleOf(PotionKind::Swiftness); + CHECK_EQ(catalog.variantTicks(swiftness), 3600); + swiftness.form = PotionItem::Form::Splash; + CHECK_EQ(catalog.variantTicks(swiftness), 2700); + swiftness.form = PotionItem::Form::Lingering; + CHECK_EQ(catalog.variantTicks(swiftness), 900); + CHECK_EQ(catalog.cloudTicks(swiftness), 600); +} + +VF_TEST(theCatalogHasExactlyOneRowPerPotionKindInEnumOrder) { + const PotionCatalog& catalog = potionCatalog(); + CHECK_EQ(catalog.size(), static_cast(PotionKind::Count)); + + for (int index = 0; index < static_cast(PotionKind::Count); ++index) { + PotionKind kind = static_cast(index); + const PotionEntry& row = catalog.entry(kind); + CHECK_EQ(row.kind, kind); + CHECK(row.name != nullptr); + CHECK(std::strlen(row.name) > 0); + } + + int duplicateNames = 0; + const std::vector& rows = catalog.entries(); + for (std::size_t i = 0; i < rows.size(); ++i) { + for (std::size_t j = i + 1; j < rows.size(); ++j) { + if (std::strcmp(rows[i].name, rows[j].name) == 0) ++duplicateNames; + } + } + CHECK_EQ(duplicateNames, 0); +} + +VF_TEST(everyCatalogDurationAndLevelMatchesWhatTheStandActuallyApplies) { + const PotionCatalog& catalog = potionCatalog(); + for (int index = 0; index < static_cast(PotionKind::Count); ++index) { + PotionKind kind = static_cast(index); + const PotionEntry& row = catalog.entry(kind); + + const struct { bool upgraded; bool extended; } modifiers[] = { + {false, false}, {true, false}, {false, true}, {true, true}}; + for (const auto& modifier : modifiers) { + PotionItem potion = bottleOf(kind, modifier.upgraded, modifier.extended); + EffectInstance applied = potionEffect(potion); + + CHECK_EQ(catalog.variantTicks(potion), applied.durationTicks); + if (row.carriesEffect()) { + CHECK_EQ(catalog.variantLevel(potion), applied.level()); + CHECK_EQ(row.effect, applied.kind); + } else { + CHECK_EQ(catalog.variantLevel(potion), 0); + CHECK_EQ(applied.kind, EffectKind::Count); + } + } + } +} + +VF_TEST(theSplashColumnOfTheCatalogMatchesWhatASplashBottleLandsAtTheImpactPoint) { + const PotionCatalog& catalog = potionCatalog(); + int checkedTimedPotions = 0; + for (const PotionEntry& row : catalog.entries()) { + if (!row.carriesEffect()) continue; + + const struct { bool upgraded; bool extended; } modifiers[] = { + {false, false}, {true, false}, {false, true}}; + for (const auto& modifier : modifiers) { + PotionItem drinkable = bottleOf(row.kind, modifier.upgraded, modifier.extended); + PotionItem splash = drinkable; + splash.form = PotionItem::Form::Splash; + + CHECK_EQ(catalog.variantTicks(splash), splashDurationThroughTheStand(drinkable)); + CHECK_EQ(catalog.variantLevel(splash), catalog.variantLevel(drinkable)); + if (!row.instant) ++checkedTimedPotions; + } + } + CHECK(checkedTimedPotions >= 30); +} + +VF_TEST(lingeringBottlesDoseAQuarterAndLeaveACloudBehindThem) { + const PotionCatalog& catalog = potionCatalog(); + + PotionItem swiftness = bottleOf(PotionKind::Swiftness); + CHECK_EQ(catalog.variantTicks(swiftness), 3600); + CHECK_EQ(catalog.cloudTicks(swiftness), 0); + + PotionItem lingering = bottleOf(PotionKind::Swiftness, false, false, + PotionItem::Form::Lingering); + CHECK_EQ(catalog.variantTicks(lingering), 900); + CHECK_EQ(catalog.cloudTicks(lingering), lingeringCloudTicks); + + PotionItem lingeringStrong = bottleOf(PotionKind::Swiftness, true, false, + PotionItem::Form::Lingering); + CHECK_EQ(catalog.variantTicks(lingeringStrong), 450); + CHECK_EQ(catalog.variantLevel(lingeringStrong), 2); +