#include #include #include "items.h" #include "player.h" #include "stores.h" #include "tables/playerdat.hpp" #include "engine/assets.hpp" #include "engine/random.hpp" namespace devilution { namespace { using ::testing::AnyOf; using ::testing::Eq; constexpr int SEED = 75357; constexpr const char MissingMpqAssetsSkipReason[] = "MPQ assets (spawn.mpq or DIABDAT.MPQ) not found - skipping test suite"; std::string itemtype_str(ItemType type); std::string misctype_str(item_misc_id type); MATCHER_P(SmithTypeMatch, i, "Valid Diablo item type from Griswold") { if (arg >= ItemType::Sword && arg <= ItemType::HeavyArmor) return true; *result_listener << "At index " << i << ": Invalid item type " << itemtype_str(arg); return false; } MATCHER_P(SmithTypeMatchHf, i, "Valid Hellfire item type from Griswold") { if (arg >= ItemType::Sword && arg <= ItemType::Staff) return true; *result_listener << "At index " << i << ": Invalid item type " << itemtype_str(arg); return false; } MATCHER_P(PremiumTypeMatch, i, "Valid premium items from Griswold") { if (arg >= ItemType::Ring && arg <= ItemType::Amulet) return true; *result_listener << "At index " << i << ": Invalid item type " << itemtype_str(arg); return false; } MATCHER_P(WitchTypeMatch, i, "Valid item type from Adria") { if (arg == ItemType::Misc || arg == ItemType::Staff) return true; *result_listener << "At index " << i << ": Invalid item type " << itemtype_str(arg); return false; } MATCHER_P(WitchMiscMatch, i, "Valid misc. item type from Adria") { if (arg >= IMISC_ELIXSTR && arg <= IMISC_ELIXVIT) return true; if (arg >= IMISC_REJUV && arg <= IMISC_FULLREJUV) return true; if (arg >= IMISC_SCROLL && arg <= IMISC_SCROLLT) return true; if (arg >= IMISC_RUNEFIRST && arg <= IMISC_RUNELAST) return true; if (arg == IMISC_BOOK) return true; *result_listener << "At index " << i << ": Invalid misc. item type " << misctype_str(arg); return false; } MATCHER_P(HealerMiscMatch, i, "Valid misc. item type from Pepin") { if (arg >= IMISC_ELIXSTR && arg <= IMISC_ELIXVIT) return true; if (arg >= IMISC_REJUV && arg <= IMISC_FULLREJUV) return true; if (arg >= IMISC_SCROLL && arg <= IMISC_SCROLLT) return true; *result_listener << "At index " << i << ": Invalid misc. item type " << misctype_str(arg); return false; } class VendorTest : public ::testing::Test { public: void SetUp() override { if (missingMpqAssets_) { GTEST_SKIP() << MissingMpqAssetsSkipReason; } Players.resize(1); MyPlayer = &Players[0]; gbIsHellfire = false; PremiumItemLevel = 1; CreatePlayer(*MyPlayer, HeroClass::Warrior); SetRndSeed(SEED); } static void SetUpTestSuite() { LoadCoreArchives(); LoadGameArchives(); missingMpqAssets_ = !HaveMainData(); if (missingMpqAssets_) { return; } LoadPlayerDataFiles(); LoadItemData(); LoadSpellData(); } private: static bool missingMpqAssets_; }; bool VendorTest::missingMpqAssets_ = false; std::string itemtype_str(ItemType type) { const std::string ITEM_TYPES[] = { "ItemType::Misc", "ItemType::Sword", "ItemType::Axe", "ItemType::Bow", "ItemType::Mace", "ItemType::Shield", "ItemType::LightArmor", "ItemType::Helm", "ItemType::MediumArmor", "ItemType::HeavyArmor", "ItemType::Staff", "ItemType::Gold", "ItemType::Ring", "ItemType::Amulet", }; if (type == ItemType::None) return "ItemType::None"; if (type < ItemType::Misc || type > ItemType::Amulet) return "ItemType does not exist!"; return ITEM_TYPES[static_cast(type)]; } std::string misctype_str(item_misc_id type) { const std::string MISC_TYPES[] = { // clang-format off "IMISC_NONE", "IMISC_USEFIRST", "IMISC_FULLHEAL", "IMISC_HEAL", "IMISC_0x4", "IMISC_0x5", "IMISC_MANA", "IMISC_FULLMANA", "IMISC_0x8", "IMISC_0x9", "IMISC_ELIXSTR", "IMISC_ELIXMAG", "IMISC_ELIXDEX", "IMISC_ELIXVIT", "IMISC_0xE", "IMISC_0xF", "IMISC_0x10", "IMISC_0x11", "IMISC_REJUV", "IMISC_FULLREJUV", "IMISC_USELAST", "IMISC_SCROLL", "IMISC_SCROLLT", "IMISC_STAFF", "IMISC_BOOK", "IMISC_RING", "IMISC_AMULET", "IMISC_UNIQUE", "IMISC_0x1C", "IMISC_OILFIRST", "IMISC_OILOF", "IMISC_OILACC", "IMISC_OILMAST", "IMISC_OILSHARP", "IMISC_OILDEATH", "IMISC_OILSKILL", "IMISC_OILBSMTH", "IMISC_OILFORT", "IMISC_OILPERM", "IMISC_OILHARD", "IMISC_OILIMP", "IMISC_OILLAST", "IMISC_MAPOFDOOM", "IMISC_EAR", "IMISC_SPECELIX", "IMISC_0x2D", "IMISC_RUNEFIRST", "IMISC_RUNEF", "IMISC_RUNEL", "IMISC_GR_RUNEL", "IMISC_GR_RUNEF", "IMISC_RUNES", "IMISC_RUNELAST", "IMISC_AURIC", "IMISC_NOTE", "IMISC_ARENAPOT" // clang-format on }; if (type == IMISC_INVALID) return "IMISC_INVALID"; if (type < IMISC_NONE || type > IMISC_ARENAPOT) return "IMISC does not exist!"; return MISC_TYPES[static_cast(type)]; } TEST_F(VendorTest, SmithGen) { MyPlayer->setCharacterLevel(25); SmithItems.clear(); SpawnSmith(16); SetRndSeed(SEED); const int N_ITEMS = RandomIntBetween(10, NumSmithBasicItems); EXPECT_EQ(SmithItems.size(), N_ITEMS); EXPECT_LE(SmithItems.size(), NumSmithBasicItems); for (size_t i = 0; i < SmithItems.size(); i++) { EXPECT_THAT(SmithItems[i]._itype, SmithTypeMatch(i)); } } TEST_F(VendorTest, SmithGenHf) { MyPlayer->setCharacterLevel(25); SmithItems.clear(); gbIsHellfire = true; SpawnSmith(16); SetRndSeed(SEED); const int N_ITEMS = RandomIntBetween(10, NumSmithBasicItemsHf); EXPECT_EQ(SmithItems.size(), N_ITEMS); EXPECT_LE(SmithItems.size(), NumSmithBasicItemsHf); for (size_t i = 0; i < SmithItems.size(); i++) { EXPECT_THAT(SmithItems[i]._itype, SmithTypeMatchHf(i)); } } TEST_F(VendorTest, PremiumQlvl1to5) { // Test starting the game as a level 1 character MyPlayer->setCharacterLevel(1); PremiumItems.clear(); SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 1, 1, 1, 1, 2, 3 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } // Test level ups MyPlayer->setCharacterLevel(5); SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 4, 4, 5, 5, 6, 7 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } } TEST_F(VendorTest, PremiumQlvl25) { constexpr int QLVLS[] = { 24, 24, 25, 25, 26, 27 }; // Test starting the game as a level 25 character MyPlayer->setCharacterLevel(25); PremiumItems.clear(); SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } // Test buying select items ReplacePremium(*MyPlayer, 0); ReplacePremium(*MyPlayer, 3); ReplacePremium(*MyPlayer, 5); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } } TEST_F(VendorTest, PremiumQlvl30Plus) { constexpr int QLVLS[] = { 30, 30, 30, 30, 30, 30 }; // Finally test level 30+ characters MyPlayer->setCharacterLevel(31); PremiumItems.clear(); SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } // Test buying select items ReplacePremium(*MyPlayer, 0); ReplacePremium(*MyPlayer, 3); ReplacePremium(*MyPlayer, 5); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } // Test 30+ levelling MyPlayer->setCharacterLevel(35); SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } // Test buying select items ReplacePremium(*MyPlayer, 0); ReplacePremium(*MyPlayer, 3); ReplacePremium(*MyPlayer, 5); EXPECT_EQ(PremiumItems.size(), NumSmithItems); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatch(i), PremiumTypeMatch(i))); } } TEST_F(VendorTest, HfPremiumQlvl1to5) { // Test level 1 character item qlvl MyPlayer->setCharacterLevel(1); PremiumItems.clear(); gbIsHellfire = true; SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } // Test level ups MyPlayer->setCharacterLevel(5); SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 8 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } } TEST_F(VendorTest, HfPremiumQlvl25) { // Test starting game as a level 25 character MyPlayer->setCharacterLevel(25); PremiumItems.clear(); gbIsHellfire = true; SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 28 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } // Test buying select items ReplacePremium(*MyPlayer, 0); ReplacePremium(*MyPlayer, 7); ReplacePremium(*MyPlayer, 14); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 24, 23, 23, 24, 24, 24, 25, 26, 25, 26, 26, 26, 27, 27, 28 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } } TEST_F(VendorTest, HfPremiumQlvl30Plus) { // Finally test level 30+ characters MyPlayer->setCharacterLevel(31); PremiumItems.clear(); gbIsHellfire = true; SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } // Test buying select items ReplacePremium(*MyPlayer, 0); ReplacePremium(*MyPlayer, 7); ReplacePremium(*MyPlayer, 14); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { constexpr int QLVLS[] = { 30, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30 }; EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } constexpr int QLVLS[] = { 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30 }; // Test 30+ levelling MyPlayer->setCharacterLevel(35); SpawnPremium(*MyPlayer); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } // Test buying select items ReplacePremium(*MyPlayer, 0); ReplacePremium(*MyPlayer, 7); ReplacePremium(*MyPlayer, 14); EXPECT_EQ(PremiumItems.size(), NumSmithItemsHf); for (size_t i = 0; i < PremiumItems.size(); i++) { EXPECT_EQ(PremiumItems[i]._iCreateInfo & CF_LEVEL, QLVLS[i]) << "Index: " << i; EXPECT_THAT(PremiumItems[i]._itype, AnyOf(SmithTypeMatchHf(i), PremiumTypeMatch(i))); } } TEST_F(VendorTest, WitchGen) { constexpr _item_indexes PINNED_ITEMS[] = { IDI_MANA, IDI_FULLMANA, IDI_PORTAL }; MyPlayer->setCharacterLevel(25); WitchItems.clear(); SpawnWitch(16); SetRndSeed(SEED); const int N_ITEMS = RandomIntBetween(10, NumWitchItems); EXPECT_EQ(WitchItems.size(), N_ITEMS); EXPECT_LE(WitchItems.size(), NumWitchItems); for (size_t i = 0; i < WitchItems.size(); i++) { if (i < NumWitchPinnedItems) { EXPECT_EQ(WitchItems[i].IDidx, PINNED_ITEMS[i]) << "Index: " << i; } else { EXPECT_THAT(WitchItems[i]._itype, WitchTypeMatch(i)); if (WitchItems[i]._itype == ItemType::Misc) { EXPECT_THAT(WitchItems[i]._iMiscId, WitchMiscMatch(i)); } } } } TEST_F(VendorTest, WitchGenHf) { constexpr _item_indexes PINNED_ITEMS[] = { IDI_MANA, IDI_FULLMANA, IDI_PORTAL }; constexpr int MAX_PINNED_BOOKS = 4; MyPlayer->setCharacterLevel(25); WitchItems.clear(); gbIsHellfire = true; SpawnWitch(16); SetRndSeed(SEED); const int N_PINNED_BOOKS = RandomIntLessThan(MAX_PINNED_BOOKS); const int N_ITEMS = RandomIntBetween(10, NumWitchItemsHf); EXPECT_EQ(WitchItems.size(), N_ITEMS); EXPECT_LE(WitchItems.size(), NumWitchItemsHf); int n_books = 0; for (size_t i = 0; i < WitchItems.size(); i++) { if (i < NumWitchPinnedItems) { EXPECT_EQ(WitchItems[i].IDidx, PINNED_ITEMS[i]) << "Index: " << i; } else { EXPECT_THAT(WitchItems[i]._itype, WitchTypeMatch(i)); if (WitchItems[i]._itype == ItemType::Misc) { EXPECT_THAT(WitchItems[i]._iMiscId, WitchMiscMatch(i)); } if (WitchItems[i]._iMiscId == IMISC_BOOK) n_books++; } } EXPECT_GE(n_books, N_PINNED_BOOKS); } TEST_F(VendorTest, HealerGen) { constexpr _item_indexes PINNED_ITEMS[] = { IDI_HEAL, IDI_FULLHEAL, IDI_RESURRECT }; MyPlayer->setCharacterLevel(25); HealerItems.clear(); SpawnHealer(16); SetRndSeed(SEED); const int N_ITEMS = RandomIntBetween(10, NumHealerItems); EXPECT_EQ(HealerItems.size(), N_ITEMS); EXPECT_LE(HealerItems.size(), NumHealerItems); for (size_t i = 0; i < HealerItems.size(); i++) { if (i < NumHealerPinnedItems) { EXPECT_EQ(HealerItems[i].IDidx, PINNED_ITEMS[i]) << "Index: " << i; } else { EXPECT_THAT(HealerItems[i]._itype, Eq(ItemType::Misc)); EXPECT_THAT(HealerItems[i]._iMiscId, HealerMiscMatch(i)); } } } TEST_F(VendorTest, HealerGenHf) { constexpr _item_indexes PINNED_ITEMS[] = { IDI_HEAL, IDI_FULLHEAL, IDI_RESURRECT }; MyPlayer->setCharacterLevel(25); HealerItems.clear(); gbIsHellfire = true; SpawnHealer(16); SetRndSeed(SEED); const int N_ITEMS = RandomIntBetween(10, NumHealerItemsHf); EXPECT_EQ(HealerItems.size(), N_ITEMS); EXPECT_LE(HealerItems.size(), NumHealerItemsHf); for (size_t i = 0; i < HealerItems.size(); i++) { if (i < NumHealerPinnedItems) { EXPECT_EQ(HealerItems[i].IDidx, PINNED_ITEMS[i]) << "Index: " << i; } else { EXPECT_THAT(HealerItems[i]._itype, Eq(ItemType::Misc)); EXPECT_THAT(HealerItems[i]._iMiscId, HealerMiscMatch(i)); } } } } // namespace } // namespace devilution