From a0dca2c9049e50e3a81608bfa2e81ed7ee8ec235 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 22:27:21 +0200 Subject: [PATCH] test: extend coverage of testLoot --- tests/testLoot.cpp | 208 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/tests/testLoot.cpp b/tests/testLoot.cpp index 92a0014..04cdf80 100644 --- a/tests/testLoot.cpp +++ b/tests/testLoot.cpp @@ -313,3 +313,211 @@ VF_TEST(lootContextForCarriesTheKillOverAndRefusesADeadSeed) { EnchantSet enchants; addEnchant(enchants, EnchantKind::Looting, 2); + HurtSource blast = environmentSource(DamageType::Explosion); + LootContext fromBlast = lootContextFor(blast, ItemId::IronSword, enchants, 0u); + CHECK_EQ(fromBlast.killedBy, DamageType::Explosion); + CHECK(!fromBlast.fiery); + CHECK(!fromBlast.playerKill); + CHECK_EQ(fromBlast.tool, ItemId::IronSword); + CHECK_EQ(fromBlast.looting(), 2); + CHECK(fromBlast.seed != 0u); + + HurtSource lava = environmentSource(DamageType::Lava); + LootContext fromLava = lootContextFor(lava, ItemId::None, EnchantSet{}, 4242u); + CHECK(fromLava.fiery); + CHECK_EQ(fromLava.seed, 4242u); + + HurtSource swing = meleeSource(1, Vec3{}, true); + LootContext fromSwing = lootContextFor(swing, ItemId::DiamondSword, EnchantSet{}, 9u); + CHECK(fromSwing.playerKill); + CHECK(!fromSwing.fiery); + CHECK_EQ(fromSwing.looting(), 0); + CHECK_EQ(fromSwing.fortune(), 0); + CHECK(!fromSwing.silkTouch()); +} + +VF_TEST(smeltedFormOnlyRewritesTheItemsThatHaveACookedCounterpart) { + CHECK_EQ(smeltedForm(ItemId::RawPorkchop), ItemId::CookedPorkchop); + CHECK_EQ(smeltedForm(ItemId::RawBeef), ItemId::CookedBeef); + CHECK_EQ(smeltedForm(ItemId::RawMutton), ItemId::CookedMutton); + CHECK_EQ(smeltedForm(ItemId::IronOre), ItemId::IronIngot); + CHECK_EQ(smeltedForm(ItemId::GoldOre), ItemId::GoldIngot); + + const ItemId unchanged[] = {ItemId::None, ItemId::Stone, ItemId::CookedBeef, + ItemId::DiamondGem, ItemId::RottenFlesh, ItemId::Bread}; + for (ItemId item : unchanged) CHECK_EQ(smeltedForm(item), item); +} + +VF_TEST(chestPoolsStayInsideTheirDeclaredRollAndCountBounds) { + const struct { LootTable::ChestKind kind; int lowest; int highest; } chests[] = { + {LootTable::ChestKind::Dungeon, 2, 16}, + {LootTable::ChestKind::Village, 3, 20}, + {LootTable::ChestKind::Temple, 2, 32}, + }; + for (const auto& chest : chests) { + int seenLowest = 1000; + int seenHighest = 0; + for (int i = 0; i < 800; ++i) { + LootRoll roll = rollChestContents(chest.kind, contextWith(ItemId::None, seedFor(i))); + seenLowest = std::min(seenLowest, roll.totalCount()); + seenHighest = std::max(seenHighest, roll.totalCount()); + } + CHECK(seenLowest >= chest.lowest); + CHECK(seenHighest <= chest.highest); + CHECK(seenHighest > seenLowest); + } +} + +VF_TEST(rareChestEntriesShowUpFarLessOftenThanCommonOnes) { + int diamonds = 0; + int iron = 0; + for (int i = 0; i < 4000; ++i) { + LootRoll roll = rollChestContents(LootTable::ChestKind::Dungeon, + contextWith(ItemId::None, seedFor(i))); + if (roll.countOf(ItemId::DiamondGem) > 0) ++diamonds; + if (roll.countOf(ItemId::IronIngot) > 0) ++iron; + } + CHECK(diamonds > 0); + CHECK(iron > diamonds * 5); +} + +VF_TEST(toolsComeOutOfAChestAsSeparateFullyRepairedStacks) { + int hoeStacks = 0; + int badStacks = 0; + for (int i = 0; i < 600; ++i) { + LootRoll roll = rollChestContents(LootTable::ChestKind::Village, + contextWith(ItemId::None, seedFor(i))); + for (const ItemSlot& stack : roll.items) { + if (stack.item != ItemId::WoodHoe) continue; + ++hoeStacks; + if (stack.count != 1) ++badStacks; + if (stack.durability != toolMaxDurability(ItemId::WoodHoe)) ++badStacks; + } + } + CHECK(hoeStacks > 0); + CHECK_EQ(badStacks, 0); +} + +VF_TEST(stacksOfTheSameMaterialMergeInsteadOfPilingUpAsSeparateEntries) { + LootPool pool; + pool.minRolls = 4; + pool.maxRolls = 4; + LootEntry ingot; + ingot.item = ItemId::IronIngot; + pool.entries.push_back(ingot); + + LootTable table; + table.pools.push_back(pool); + + LootRoll roll = rollLootTable(table, contextWith(ItemId::None, 77u)); + CHECK_EQ(roll.items.size(), std::size_t{1}); + CHECK_EQ(roll.countOf(ItemId::IronIngot), 4); + CHECK_EQ(roll.totalCount(), 4); + CHECK_EQ(roll.countOf(ItemId::GoldIngot), 0); +} + +VF_TEST(killConditionsGateEntriesOnHowTheTargetActuallyDied) { + LootTable table = singlePool({entryOf(ItemId::Gunpowder, LootEntry::Explosive), + entryOf(ItemId::Stick, LootEntry::Ranged)}); + + LootContext blasted = contextWith(ItemId::None, 13u); + blasted.killedBy = DamageType::Explosion; + LootRoll fromBlast = rollLootTable(table, blasted); + CHECK_EQ(fromBlast.countOf(ItemId::Gunpowder), 1); + CHECK_EQ(fromBlast.countOf(ItemId::Stick), 0); + + LootContext shot = contextWith(ItemId::None, 13u); + shot.killedBy = DamageType::Projectile; + LootRoll fromArrow = rollLootTable(table, shot); + CHECK_EQ(fromArrow.countOf(ItemId::Stick), 1); + CHECK_EQ(fromArrow.countOf(ItemId::Gunpowder), 0); + + LootContext punched = contextWith(ItemId::None, 13u); + punched.killedBy = DamageType::Melee; + CHECK(rollLootTable(table, punched).empty()); +} + +VF_TEST(enchantmentConditionsGateEntriesOnTheToolThatWasUsed) { + LootTable table = singlePool({entryOf(ItemId::EmeraldGem, LootEntry::NeedsLooting), + entryOf(ItemId::LapisLazuli, LootEntry::NeedsFortune), + entryOf(ItemId::Stick, LootEntry::NotSilkTouched)}); + + LootContext bare = contextWith(ItemId::None, 21u); + LootRoll plain = rollLootTable(table, bare); + CHECK_EQ(plain.countOf(ItemId::Stick), 1); + CHECK_EQ(plain.totalCount(), 1); + + LootContext silk = enchanted(ItemId::IronPickaxe, EnchantKind::SilkTouch, 1, 21u); + CHECK(rollLootTable(table, silk).empty()); + + LootContext lucky = enchanted(ItemId::IronPickaxe, EnchantKind::Fortune, 2, 21u); + LootRoll withFortune = rollLootTable(table, lucky); + CHECK_EQ(withFortune.totalCount(), 1); + CHECK(withFortune.countOf(ItemId::LapisLazuli) + withFortune.countOf(ItemId::Stick) == 1); + CHECK_EQ(withFortune.countOf(ItemId::EmeraldGem), 0); +} + +VF_TEST(theSmeltIfFieryFunctionRewritesTheItemAtEmitTime) { + LootEntry beef; + beef.item = ItemId::RawBeef; + beef.minCount = 2; + beef.maxCount = 2; + beef.functions = LootEntry::SmeltIfFiery; + LootTable table = singlePool({beef}); + + LootContext cold = contextWith(ItemId::None, 5u); + LootRoll raw = rollLootTable(table, cold); + CHECK_EQ(raw.countOf(ItemId::RawBeef), 2); + + LootContext burning = contextWith(ItemId::None, 5u); + burning.fiery = true; + LootRoll cooked = rollLootTable(table, burning); + CHECK_EQ(cooked.countOf(ItemId::CookedBeef), 2); + CHECK_EQ(cooked.countOf(ItemId::RawBeef), 0); +} + +VF_TEST(bonusRollsPerLootingAddWholeExtraPullsToAPool) { + LootPool pool; + pool.bonusRollsPerLooting = 2; + LootEntry gold; + gold.item = ItemId::GoldIngot; + pool.entries.push_back(gold); + LootTable table; + table.pools.push_back(pool); + + CHECK_EQ(rollLootTable(table, contextWith(ItemId::None, 3u)).countOf(ItemId::GoldIngot), 1); + + LootContext looted = enchanted(ItemId::IronSword, EnchantKind::Looting, 3, 3u); + CHECK_EQ(rollLootTable(table, looted).countOf(ItemId::GoldIngot), 7); +} + +VF_TEST(anEmptyOrOutOfRangeTableRollsToNothingInsteadOfReadingPastItsEnd) { + LootContext context = contextWith(ItemId::DiamondPickaxe, 1u); + CHECK(rollLootTable(LootTable{}, context).empty()); + + CHECK(blockLootTable(Block::Count).empty()); + CHECK(mobLootTable(LootTable::MobKind::Count).empty()); + CHECK(chestLootTable(LootTable::ChestKind::Count).empty()); + + CHECK(rollMobDrops(LootTable::MobKind::Count, context).empty()); + CHECK(rollChestContents(LootTable::ChestKind::Count, context).empty()); + + // Air has no item form, so its self-drop table comes out empty rather than + // dropping a phantom stack. + CHECK(rollBlockDrops(Block::Air, context).empty()); +} + +VF_TEST(countOfAndTotalCountAgreeWithTheStacksTheyWereBuiltFrom) { + LootRoll roll; + roll.items.push_back({ItemId::IronIngot, 3, 0}); + roll.items.push_back({ItemId::Bread, 2, 0}); + roll.items.push_back({ItemId::IronIngot, 4, 0}); + + CHECK_EQ(roll.countOf(ItemId::IronIngot), 7); + CHECK_EQ(roll.countOf(ItemId::Bread), 2); + CHECK_EQ(roll.countOf(ItemId::DiamondGem), 0); + CHECK_EQ(roll.totalCount(), 9); + CHECK(!roll.empty()); + CHECK(LootRoll{}.empty()); + CHECK_EQ(LootRoll{}.totalCount(), 0); +}