test: extend coverage of testLoot

This commit is contained in:
dualfroz committed 2026-09-11 22:27:21 +02:00
1 parent 80ef7386dc
commit a0dca2c904
1 file changed
+208
+208
View File
@@ -313,3 +313,211 @@ VF_TEST(lootContextForCarriesTheKillOverAndRefusesADeadSeed) {
EnchantSet enchants; EnchantSet enchants;
addEnchant(enchants, EnchantKind::Looting, 2); 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);
}