From 1677d1115041ae15f14c3e096d39da96fc5d85c9 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 18:05:49 +0200 Subject: [PATCH] test: cover testEquipment --- tests/testEquipment.cpp | 220 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/tests/testEquipment.cpp b/tests/testEquipment.cpp index 8abb21a..c1c9f15 100644 --- a/tests/testEquipment.cpp +++ b/tests/testEquipment.cpp @@ -157,3 +157,223 @@ VF_TEST(theHeldToolDecidesAttackDamageSpeedAndReach) { CHECK_NEAR(stats.attackReach, unarmedAttackReach + 0.5f, 0.0001f); CHECK_NEAR(modifierTotal(stats, AttributeModifier::Attribute::AttackDamage), 6.0f, 0.0001f); + CombatStats combat; + stats.applyTo(combat); + CHECK_NEAR(combat.baseAttackDamage, 7.0f, 0.0001f); + CHECK_NEAR(combat.attackSpeed, 1.6f, 0.0001f); +} + +VF_TEST(sharpnessReachesCombatThroughTheEnchantSetAndNotThroughAttackDamage) { + Equipment plain; + plain.equip(EquipSlot::MainHand, makeTool(ItemId::DiamondSword)); + + Equipment sharp; + sharp.equip(EquipSlot::MainHand, + makeEnchantedTool(ItemId::DiamondSword, withEnchant(EnchantKind::Sharpness, 5))); + + // The item value is identical; only the enchantment set differs. + CHECK_NEAR(plain.stats().attackDamage, sharp.stats().attackDamage, 0.0001f); + CHECK(plain.stats().weaponEnchants.empty()); + CHECK_EQ(sharp.stats().weaponEnchants.levelOf(EnchantKind::Sharpness), 5); + + EffectSet none; + CombatStats attacker = readyToSwing(); + float plainSwing = equippedMeleeDamage(plain, attacker, none, false, false, false); + float sharpSwing = equippedMeleeDamage(sharp, attacker, none, false, false, false); + CHECK(sharpSwing > plainSwing); + CHECK_NEAR(sharpSwing - plainSwing, 3.0f, 0.05f); + + Equipment unarmed; + CHECK(equippedMeleeDamage(unarmed, attacker, none, false, false, false) < plainSwing); +} + +VF_TEST(efficiencyRaisesMiningSpeedWhileTheCombatNumbersStayPut) { + Equipment plain; + plain.equip(EquipSlot::MainHand, makeTool(ItemId::DiamondPickaxe)); + + Equipment quick; + quick.equip(EquipSlot::MainHand, + makeEnchantedTool(ItemId::DiamondPickaxe, withEnchant(EnchantKind::Efficiency, 4))); + + EquipmentStats plainStats = plain.stats(); + EquipmentStats quickStats = quick.stats(); + + CHECK_NEAR(plainStats.miningSpeed, toolTierSpeedMultiplier(ToolTier::Diamond), 0.0001f); + CHECK_NEAR(quickStats.miningSpeed, plainStats.miningSpeed + 17.0f, 0.0001f); + CHECK_NEAR(quickStats.attackDamage, plainStats.attackDamage, 0.0001f); + CHECK(modifierTotal(quickStats, AttributeModifier::Attribute::MiningSpeed) > + modifierTotal(plainStats, AttributeModifier::Attribute::MiningSpeed)); +} + +VF_TEST(armorWeightCostsMovementSpeedInProportionToItsPoints) { + Equipment bare; + CHECK_NEAR(bare.stats().movementSpeed, 1.0f, 0.0001f); + + Equipment leather; + leather.equipArmor(makeArmor(Armor::Material::Leather, Armor::Slot::Helmet)); + Equipment diamond = fullDiamond(); + + CHECK(leather.stats().movementSpeed < 1.0f); + CHECK(diamond.stats().movementSpeed < leather.stats().movementSpeed); + CHECK_NEAR(diamond.stats().movementSpeed, 1.0f - 0.20f, 0.0001f); + CHECK_NEAR(modifierTotal(diamond.stats(), AttributeModifier::Attribute::MovementSpeed), -0.20f, + 0.0001f); + CHECK_NEAR(armorWeightPenalty(0), 0.0f, 0.0001f); + CHECK_NEAR(armorWeightPenalty(100), 0.35f, 0.0001f); +} + +VF_TEST(armorWearsDownWhenHitsLandAndAPieceVanishesAtZero) { + Equipment gear; + Armor helmet = makeArmor(Armor::Material::Leather, Armor::Slot::Helmet); + gear.equipArmor(helmet); + int startingDurability = gear.armorPiece(EquipSlot::Head).durability; + CHECK(startingDurability > 0); + + uint32_t rng = 8u; + int spent = gear.wearFromHit(8.0f, rng); + CHECK(spent > 0); + CHECK_EQ(gear.armorPiece(EquipSlot::Head).durability, startingDurability - spent); + + int guard = 0; + while (gear.wornPieces() > 0 && guard < 5000) { + gear.wearFromHit(20.0f, rng); + ++guard; + } + CHECK(guard < 5000); + CHECK_EQ(gear.wornPieces(), 0); + CHECK(gear.armorPiece(EquipSlot::Head).material == Armor::Material::None); + CHECK_EQ(gear.stats().armorPoints, 0); +} + +VF_TEST(unbreakingArmorSurvivesNoticeablyMoreHits) { + uint32_t plainRng = 31u; + Equipment plain; + plain.equipArmor(makeArmor(Armor::Material::Iron, Armor::Slot::Chestplate)); + int plainHits = 0; + while (plain.wornPieces() > 0 && plainHits < 20000) { + plain.wearFromHit(4.0f, plainRng); + ++plainHits; + } + + uint32_t toughRng = 31u; + Equipment tough; + Armor reinforced = makeArmor(Armor::Material::Iron, Armor::Slot::Chestplate); + addEnchant(reinforced.enchants, EnchantKind::Unbreaking, 3); + tough.equipArmor(reinforced); + int toughHits = 0; + while (tough.wornPieces() > 0 && toughHits < 20000) { + tough.wearFromHit(4.0f, toughRng); + ++toughHits; + } + + CHECK(plainHits > 0); + CHECK(toughHits > plainHits * 2); + CHECK(toughHits < 20000); +} + +VF_TEST(theHeldToolBreaksAtZeroUnlessItIsUnbreakable) { + uint32_t rng = 17u; + Equipment gear; + gear.equip(EquipSlot::MainHand, makeTool(ItemId::WoodShovel)); + int max = gear.mainHand().meta.durability.max; + + for (int hit = 1; hit < max; ++hit) { + CHECK(gear.useHeld(EquipSlot::MainHand, 1, rng) == WearResult::Damaged); + } + CHECK(gear.useHeld(EquipSlot::MainHand, 1, rng) == WearResult::Broke); + CHECK(gear.mainHand().empty()); + + MetaStack indestructible = makeTool(ItemId::WoodShovel); + indestructible.meta.unbreakable = true; + gear.equip(EquipSlot::OffHand, indestructible); + CHECK(gear.useHeld(EquipSlot::OffHand, 5000, rng) == WearResult::Absorbed); + CHECK(!gear.offHand().empty()); + CHECK(gear.useHeld(EquipSlot::Head, 1, rng) == WearResult::NotDamageable); +} + +VF_TEST(experiencePickedUpRepairsMendingGearBeforeTheHeldTool) { + Equipment gear; + Armor boots = makeArmor(Armor::Material::Iron, Armor::Slot::Boots); + addEnchant(boots.enchants, EnchantKind::Mending, 1); + boots.durability = boots.maxDurability - 30; + gear.equipArmor(boots); + + MetaStack pickaxe = makeEnchantedTool(ItemId::IronPickaxe, withEnchant(EnchantKind::Mending, 1)); + pickaxe.meta.durability.used = 100; + gear.equip(EquipSlot::MainHand, pickaxe); + + int repaired = gear.absorbExperience(10); + CHECK_EQ(repaired, 20); + CHECK_EQ(gear.armorPiece(EquipSlot::Feet).durability, boots.maxDurability - 10); + CHECK_EQ(gear.mainHand().meta.durability.used, 100); + + int spilled = gear.absorbExperience(100); + CHECK_EQ(gear.armorPiece(EquipSlot::Feet).durability, boots.maxDurability); + CHECK(gear.mainHand().meta.durability.used < 100); + CHECK(spilled > 10); + + Equipment plain; + plain.equipArmor(makeArmor(Armor::Material::Iron, Armor::Slot::Boots)); + CHECK_EQ(plain.absorbExperience(50), 0); + CHECK_EQ(plain.absorbExperience(0), 0); +} + +VF_TEST(theHandsRoundTripThroughAnOpenMenu) { + ContainerMenu menu(MenuKind::PlayerInventory, &sharedRegistry()); + const MenuLayout& layout = menu.layout(); + + MetaStack sword = makeEnchantedTool(ItemId::IronSword, withEnchant(EnchantKind::FireAspect, 2)); + sword.meta.customName = "Emberfang"; + menu.slot(layout.hotbarFirst + 4).cell.stack = sword; + menu.slot(layout.offHandSlot).cell.stack = makeStack(ItemId::Bread, 5); + + Equipment gear; + loadHandsFromMenu(gear, menu, 4); + CHECK(stacksIdentical(gear.mainHand(), sword)); + CHECK_EQ(gear.offHand().count, 5); + CHECK_EQ(gear.stats().weaponEnchants.levelOf(EnchantKind::FireAspect), 2); + + uint32_t rng = 4u; + gear.useHeld(EquipSlot::MainHand, 3, rng); + storeHandsIntoMenu(gear, menu, 4); + CHECK_EQ(menu.stackAt(layout.hotbarFirst + 4).meta.durability.used, 3); + CHECK(menu.stackAt(layout.hotbarFirst + 4).meta.customName == std::string("Emberfang")); + + // A chest screen has no off-hand slot, and loading from it leaves that hand + // exactly as it was. + ContainerMenu chest(MenuKind::Chest, &sharedRegistry()); + chest.slot(chest.layout().hotbarFirst).cell.stack = makeStack(ItemId::Stone, 2); + loadHandsFromMenu(gear, chest, 0); + CHECK(gear.mainHand().item == ItemId::Stone); + CHECK_EQ(gear.offHand().count, 5); + + loadHandsFromMenu(gear, chest, 99); + CHECK(gear.mainHand().item == ItemId::Stone); +} + +VF_TEST(theModifierListAddsUpToTheStatsItIsDescribing) { + Equipment gear = fullDiamond(); + gear.equip(EquipSlot::MainHand, + makeEnchantedTool(ItemId::IronAxe, withEnchant(EnchantKind::Efficiency, 2))); + EquipmentStats stats = gear.stats(); + + CHECK_NEAR(unarmedAttackDamage + + modifierTotal(stats, AttributeModifier::Attribute::AttackDamage), + stats.attackDamage, 0.0001f); + CHECK_NEAR(unarmedAttackSpeed + modifierTotal(stats, AttributeModifier::Attribute::AttackSpeed), + stats.attackSpeed, 0.0001f); + CHECK_NEAR(unarmedAttackReach + modifierTotal(stats, AttributeModifier::Attribute::AttackReach), + stats.attackReach, 0.0001f); + CHECK_NEAR(1.0f + modifierTotal(stats, AttributeModifier::Attribute::MiningSpeed), + stats.miningSpeed, 0.0001f); + CHECK_NEAR(1.0f + modifierTotal(stats, AttributeModifier::Attribute::MovementSpeed), + stats.movementSpeed, 0.0001f); + CHECK_NEAR(modifierTotal(stats, AttributeModifier::Attribute::ArmorPoints), + static_cast(stats.armorPoints), 0.0001f); + + int handModifiers = 0; + for (const AttributeModifier& modifier : stats.modifiers) { + if (modifier.slot == EquipSlot::MainHand) ++handModifiers; + } + CHECK(handModifiers >= 4); +}