diff --git a/tests/testLoot.cpp b/tests/testLoot.cpp index 89820f5..92a0014 100644 --- a/tests/testLoot.cpp +++ b/tests/testLoot.cpp @@ -156,3 +156,160 @@ VF_TEST(fortuneMultipliesOreDropsAndNeverDropsBelowTheBaseYield) { CHECK_EQ(worstPlain, 1); CHECK_EQ(worstFortune, 1); + CHECK_EQ(maxOverSeeds(Block::DiamondOre, bare, ItemId::DiamondPickaxe, ItemId::DiamondGem, 400), + 1); + CHECK_EQ(maxOverSeeds(Block::DiamondOre, fortune, ItemId::DiamondPickaxe, ItemId::DiamondGem, + 400), + 4); + + // Silk Touch wins over Fortune: the ore block itself, never a multiplied gem. + LootContext both = enchanted(ItemId::DiamondPickaxe, EnchantKind::SilkTouch, 1, 99u); + addEnchant(both.toolEnchants, EnchantKind::Fortune, 3); + LootRoll roll = rollBlockDrops(Block::DiamondOre, both); + CHECK_EQ(roll.countOf(ItemId::DiamondOre), 1); + CHECK_EQ(roll.countOf(ItemId::DiamondGem), 0); +} + +VF_TEST(grassYieldsDirtAndBedrockYieldsNothingAtAll) { + LootContext shovel = contextWith(ItemId::IronShovel, 5u); + LootRoll grass = rollBlockDrops(Block::Grass, shovel); + CHECK_EQ(grass.countOf(ItemId::Dirt), 1); + CHECK_EQ(grass.countOf(ItemId::Grass), 0); + + LootContext pickaxe = contextWith(ItemId::DiamondPickaxe, 5u); + CHECK(rollBlockDrops(Block::Bedrock, pickaxe).empty()); + CHECK(blockLootTable(Block::Bedrock).empty()); + + LootContext silk = enchanted(ItemId::DiamondPickaxe, EnchantKind::SilkTouch, 1, 5u); + CHECK(rollBlockDrops(Block::Bedrock, silk).empty()); +} + +VF_TEST(leavesOnlyDropTheirSubstituteCuttingWhenSilkTouchIsAbsent) { + LootContext silk = enchanted(ItemId::IronHoe, EnchantKind::SilkTouch, 1, 3u); + LootRoll harvested = rollBlockDrops(Block::Leaves, silk); + CHECK_EQ(harvested.countOf(ItemId::Leaves), 1); + CHECK_EQ(harvested.totalCount(), 1); + + int cuttings = 0; + int cuttingsWithFortune = 0; + int leafBlocks = 0; + EnchantSet fortune; + addEnchant(fortune, EnchantKind::Fortune, 3); + for (int i = 0; i < 1500; ++i) { + LootContext plain = contextWith(ItemId::IronHoe, seedFor(i)); + LootRoll roll = rollBlockDrops(Block::Leaves, plain); + cuttings += roll.countOf(ItemId::Wood); + leafBlocks += roll.countOf(ItemId::Leaves); + + LootContext lucky = contextWith(ItemId::IronHoe, seedFor(i)); + lucky.toolEnchants = fortune; + cuttingsWithFortune += rollBlockDrops(Block::Leaves, lucky).countOf(ItemId::Wood); + } + CHECK_EQ(leafBlocks, 0); + CHECK(cuttings > 0); + CHECK(cuttingsWithFortune > cuttings * 2); +} + +VF_TEST(toolTierGatesTheOresTheWayTheHarvestLevelsSay) { + CHECK(!toolQualifiesFor(Block::Stone, ItemId::None)); + CHECK(!toolQualifiesFor(Block::Stone, ItemId::IronAxe)); + CHECK(toolQualifiesFor(Block::Stone, ItemId::WoodPickaxe)); + + CHECK(!toolQualifiesFor(Block::IronOre, ItemId::WoodPickaxe)); + CHECK(!toolQualifiesFor(Block::IronOre, ItemId::GoldPickaxe)); + CHECK(toolQualifiesFor(Block::IronOre, ItemId::StonePickaxe)); + + CHECK(!toolQualifiesFor(Block::DiamondOre, ItemId::StonePickaxe)); + CHECK(!toolQualifiesFor(Block::DiamondOre, ItemId::GoldPickaxe)); + CHECK(toolQualifiesFor(Block::DiamondOre, ItemId::IronPickaxe)); + CHECK(toolQualifiesFor(Block::EmeraldOre, ItemId::DiamondPickaxe)); + + // Nothing outside the pickaxe class is gated at all. + CHECK(toolQualifiesFor(Block::Dirt, ItemId::None)); + CHECK(toolQualifiesFor(Block::Leaves, ItemId::None)); + CHECK(toolQualifiesFor(Block::Grass, ItemId::WoodShovel)); +} + +VF_TEST(aBlockBrokenWithTooWeakAToolProducesNoDropsAtAll) { + LootContext weak = contextWith(ItemId::StonePickaxe, 11u); + CHECK(rollBlockDrops(Block::DiamondOre, weak).empty()); + + LootContext bare = contextWith(ItemId::None, 11u); + CHECK(rollBlockDrops(Block::Stone, bare).empty()); + + // The gate runs before the table, so even Silk Touch cannot rescue it. + LootContext silkOnAStonePick = enchanted(ItemId::StonePickaxe, EnchantKind::SilkTouch, 1, 11u); + CHECK(rollBlockDrops(Block::DiamondOre, silkOnAStonePick).empty()); +} + +VF_TEST(zombiesDropFleshAlwaysAndIronOnlyWhenAPlayerLandedTheKill) { + int ingotsFromPlayers = 0; + int ingotsFromTheWorld = 0; + int fleshTotal = 0; + for (int i = 0; i < 3000; ++i) { + LootRoll byPlayer = rollMobDrops(LootTable::MobKind::Zombie, killContext(true, seedFor(i))); + LootRoll byWorld = rollMobDrops(LootTable::MobKind::Zombie, killContext(false, seedFor(i))); + ingotsFromPlayers += byPlayer.countOf(ItemId::IronIngot); + ingotsFromTheWorld += byWorld.countOf(ItemId::IronIngot); + fleshTotal += byPlayer.countOf(ItemId::RottenFlesh); + } + CHECK_EQ(ingotsFromTheWorld, 0); + CHECK(ingotsFromPlayers > 0); + CHECK(fleshTotal > 2000); + CHECK(fleshTotal < 4500); +} + +VF_TEST(lootingRaisesBothTheDropCeilingAndTheRareDropRate) { + int plainCeiling = 0; + int lootedCeiling = 0; + int plainIngots = 0; + int lootedIngots = 0; + for (int i = 0; i < 3000; ++i) { + LootContext plain = killContext(true, seedFor(i)); + LootContext looted = killContext(true, seedFor(i)); + addEnchant(looted.toolEnchants, EnchantKind::Looting, 3); + + LootRoll plainRoll = rollMobDrops(LootTable::MobKind::Zombie, plain); + LootRoll lootedRoll = rollMobDrops(LootTable::MobKind::Zombie, looted); + plainCeiling = std::max(plainCeiling, plainRoll.countOf(ItemId::RottenFlesh)); + lootedCeiling = std::max(lootedCeiling, lootedRoll.countOf(ItemId::RottenFlesh)); + plainIngots += plainRoll.countOf(ItemId::IronIngot); + lootedIngots += lootedRoll.countOf(ItemId::IronIngot); + } + CHECK_EQ(plainCeiling, 2); + CHECK_EQ(lootedCeiling, 5); + CHECK(lootedIngots > plainIngots); +} + +VF_TEST(livestockDropRawMeatThatComesOutCookedWhenTheKillWasFiery) { + const struct { LootTable::MobKind kind; ItemId raw; ItemId cooked; int ceiling; } herd[] = { + {LootTable::MobKind::Sheep, ItemId::RawMutton, ItemId::CookedMutton, 2}, + {LootTable::MobKind::Cow, ItemId::RawBeef, ItemId::CookedBeef, 3}, + {LootTable::MobKind::Pig, ItemId::RawPorkchop, ItemId::CookedPorkchop, 3}, + }; + for (const auto& animal : herd) { + int rawSeen = 0; + int cookedSeen = 0; + int highest = 0; + for (int i = 0; i < 300; ++i) { + LootContext plain = killContext(true, seedFor(i)); + LootRoll rawRoll = rollMobDrops(animal.kind, plain); + rawSeen += rawRoll.countOf(animal.raw); + highest = std::max(highest, rawRoll.countOf(animal.raw)); + CHECK_EQ(rawRoll.countOf(animal.cooked), 0); + + LootContext burning = killContext(true, seedFor(i)); + burning.fiery = true; + LootRoll cookedRoll = rollMobDrops(animal.kind, burning); + cookedSeen += cookedRoll.countOf(animal.cooked); + CHECK_EQ(cookedRoll.countOf(animal.raw), 0); + } + CHECK_EQ(rawSeen, cookedSeen); + CHECK_EQ(highest, animal.ceiling); + } +} + +VF_TEST(lootContextForCarriesTheKillOverAndRefusesADeadSeed) { + EnchantSet enchants; + addEnchant(enchants, EnchantKind::Looting, 2); +