From 5d384bf6bdff57e18dddf145cd715460a8103eaf Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 15:51:53 +0200 Subject: [PATCH] test: cover testDamage --- tests/testDamage.cpp | 224 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/tests/testDamage.cpp b/tests/testDamage.cpp index b325ac3..f07c7d1 100644 --- a/tests/testDamage.cpp +++ b/tests/testDamage.cpp @@ -157,3 +157,227 @@ VF_TEST(strengthAddsAndWeaknessRemovesFlatMeleeDamage) { applyEffect(weak, makeEffect(EffectKind::Weakness, 1, 200)); CHECK_NEAR(meleeAttackDamage(attacker, bare, weak, false, false, false), 3.0f, 0.001f); + EffectSet crippled; + applyEffect(crippled, makeEffect(EffectKind::Weakness, 3, 200)); + CHECK_NEAR(meleeAttackDamage(attacker, bare, crippled, false, false, false), 0.0f, 0.001f); +} + +VF_TEST(resistanceAndProtectionMultiplyOntoTheArmorReducedHit) { + CHECK_NEAR(protectionFactor(0), 1.0f, 0.001f); + CHECK_NEAR(protectionFactor(16), 0.36f, 0.001f); + CHECK_NEAR(protectionFactor(40), 0.2f, 0.001f); + + uint32_t rng = 21u; + CombatStats player; + player.armor = fullSet(Armor::Material::Leather); + enchantWholeSet(player.armor, EnchantKind::Protection, 4); + CHECK_EQ(player.armor.protectionPoints(DamageType::Melee), 16); + + EffectSet resisted; + applyEffect(resisted, makeEffect(EffectKind::Resistance, 2, 400)); + + CombatResult hit = applyDamage(player, resisted, hitFor(DamageType::Melee, 12.0f), rng); + CHECK(hit.applied); + CHECK(hit.afterArmor < 12.0f); + CHECK_NEAR(hit.afterResistance, hit.afterArmor * 0.6f, 0.001f); + CHECK_NEAR(hit.afterProtection, hit.afterResistance * 0.36f, 0.001f); + CHECK_NEAR(hit.healthLost, hit.afterProtection, 0.001f); +} + +VF_TEST(featherFallingOnlyHelpsAgainstFallDamage) { + ArmorSet boots; + Armor feet = makeArmor(Armor::Material::Iron, Armor::Slot::Boots); + CHECK(addEnchant(feet.enchants, EnchantKind::FeatherFalling, 4)); + boots.equip(feet); + + CHECK_EQ(boots.protectionPoints(DamageType::Fall), 12); + CHECK_EQ(boots.protectionPoints(DamageType::Melee), 0); + CHECK_EQ(boots.protectionPoints(DamageType::Explosion), 0); +} + +VF_TEST(fallDamageScalesWithDistanceAndJumpBoostRaisesTheSafeDrop) { + EffectSet none; + CHECK_NEAR(fallDamage(2.0f, none), 0.0f, 0.001f); + CHECK_NEAR(fallDamage(3.0f, none), 0.0f, 0.001f); + CHECK_NEAR(fallDamage(10.0f, none), 7.0f, 0.001f); + CHECK_NEAR(fallDamage(23.5f, none), 20.0f, 0.001f); + + EffectSet springy; + applyEffect(springy, makeEffect(EffectKind::JumpBoost, 2, 200)); + CHECK_NEAR(fallDamage(10.0f, springy), 5.0f, 0.001f); + CHECK_NEAR(fallDamage(5.0f, springy), 0.0f, 0.001f); +} + +VF_TEST(armorWearsDownAndFallsOffAfterSustainedPunishment) { + EffectSet none; + uint32_t rng = 4242u; + CombatStats player; + player.armor = fullSet(Armor::Material::Iron); + int startingPoints = player.armor.totalPoints(); + CHECK_EQ(startingPoints, 15); + + int helmetStart = player.armor.at(Armor::Slot::Helmet).durability; + CHECK_EQ(helmetStart, 165); + + for (int hit = 0; hit < 200; ++hit) { + player.health = 20.0f; + player.alive = true; + applyDamage(player, none, hitFor(DamageType::Melee, 8.0f), rng); + idleTicks(player, none, invulnerabilityTicks); + } + CHECK(!player.armor.at(Armor::Slot::Helmet).worn()); + CHECK_EQ(player.armor.totalPoints(), 0); +} + +VF_TEST(unbreakingStretchesArmorDurabilityOverManyHits) { + uint32_t plainRng = 99u; + ArmorSet plain; + plain.equip(makeArmor(Armor::Material::Iron, Armor::Slot::Chestplate)); + + uint32_t toughRng = 99u; + ArmorSet tough; + Armor chest = makeArmor(Armor::Material::Iron, Armor::Slot::Chestplate); + CHECK(addEnchant(chest.enchants, EnchantKind::Unbreaking, 3)); + tough.equip(chest); + + int plainSpent = 0; + int toughSpent = 0; + for (int hit = 0; hit < 40; ++hit) { + plainSpent += wearArmor(plain, 8.0f, plainRng); + toughSpent += wearArmor(tough, 8.0f, toughRng); + } + CHECK_EQ(plainSpent, 80); + CHECK(toughSpent > 0); + CHECK(toughSpent * 2 < plainSpent); +} + +VF_TEST(absorptionSoaksDamageBeforeHealthDoes) { + EffectSet none; + uint32_t rng = 5u; + CombatStats player; + player.absorption = 4.0f; + + CombatResult hit = applyDamage(player, none, hitFor(DamageType::Magic, 6.0f), rng); + CHECK_NEAR(hit.absorbedByAbsorption, 4.0f, 0.001f); + CHECK_NEAR(hit.healthLost, 2.0f, 0.001f); + CHECK_NEAR(player.absorption, 0.0f, 0.001f); + CHECK_NEAR(player.health, 18.0f, 0.001f); +} + +VF_TEST(absorptionEffectRefillsTheShieldOnACombatTick) { + CombatStats player; + EffectSet effects; + applyEffect(effects, makeEffect(EffectKind::Absorption, 2, 400)); + applyEffect(effects, makeEffect(EffectKind::HealthBoost, 1, 400)); + + tickCombat(player, effects); + CHECK_NEAR(player.absorption, 8.0f, 0.001f); + CHECK_NEAR(effectiveMaxHealth(player, effects), 24.0f, 0.001f); +} + +VF_TEST(knockbackPushesAwayFromTheAttackerAndScalesWithTheEnchant) { + Vec3 gentle = knockbackVector({0.0f, 0.0f, 0.0f}, {3.0f, 0.0f, 0.0f}, 0.0f); + CHECK_NEAR(gentle.x, 0.4f, 0.001f); + CHECK_NEAR(gentle.y, 0.4f, 0.001f); + CHECK_NEAR(gentle.z, 0.0f, 0.001f); + + Vec3 hard = knockbackVector({0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, -2.0f}, 2.0f); + CHECK_NEAR(hard.z, -1.4f, 0.001f); + CHECK_NEAR(hard.x, 0.0f, 0.001f); + + EffectSet none; + uint32_t rng = 8u; + CombatStats target; + DamageEvent event; + event.source = meleeSource(1, {0.0f, 0.0f, 0.0f}, true); + event.amount = 4.0f; + event.direction = {1.0f, 0.0f, 0.0f}; + event.knockbackStrength = 1.0f; + CombatResult hit = applyDamage(target, none, event, rng); + CHECK_NEAR(hit.knockback.x, 0.9f, 0.001f); +} + +VF_TEST(thornsRetaliatesOnSomeHitsAndNeverExceedsFourDamage) { + EffectSet none; + uint32_t rng = 1234u; + CombatStats player; + player.armor = fullSet(Armor::Material::Diamond); + enchantWholeSet(player.armor, EnchantKind::Thorns, 3); + + int retaliations = 0; + for (int hit = 0; hit < 300; ++hit) { + player.health = 20.0f; + player.alive = true; + player.invulnerableTicks = 0; + DamageEvent event; + event.source = meleeSource(2, {0.0f, 0.0f, 0.0f}, true); + event.amount = 5.0f; + event.direction = {0.0f, 0.0f, 1.0f}; + CombatResult struck = applyDamage(player, none, event, rng); + if (struck.thornsDamage > 0.0f) { + ++retaliations; + CHECK(struck.thornsDamage >= 1.0f); + CHECK(struck.thornsDamage <= 4.0f); + } + } + CHECK(retaliations > 60); + CHECK(retaliations < 240); +} + +VF_TEST(environmentalDamageWithoutAnAttackerNeverTriggersThorns) { + EffectSet none; + uint32_t rng = 77u; + CombatStats player; + player.armor = fullSet(Armor::Material::Diamond); + enchantWholeSet(player.armor, EnchantKind::Thorns, 3); + + for (int tick = 0; tick < 100; ++tick) { + player.health = 20.0f; + player.alive = true; + player.invulnerableTicks = 0; + CombatResult burn = applyDamage(player, none, hitFor(DamageType::Cactus, 2.0f), rng); + CHECK_NEAR(burn.thornsDamage, 0.0f, 0.001f); + } +} + +VF_TEST(fireResistanceMakesLavaAndFireTicksHarmless) { + uint32_t rng = 6u; + EffectSet fireproof; + applyEffect(fireproof, makeEffect(EffectKind::FireResistance, 1, 400)); + + CombatStats player; + CombatResult lava = applyDamage(player, fireproof, hitFor(DamageType::Lava, 4.0f), rng); + CHECK(!lava.applied); + CHECK_NEAR(player.health, 20.0f, 0.001f); + + CombatResult drown = applyDamage(player, fireproof, hitFor(DamageType::Drowning, 2.0f), rng); + CHECK(drown.applied); + CHECK_NEAR(player.health, 18.0f, 0.001f); +} + +VF_TEST(witherEffectRunsAPlayerDownThroughRepeatedCombatTicks) { + CombatStats player; + EffectSet effects; + applyEffect(effects, makeEffect(EffectKind::Wither, 2, 2000)); + + int tick = 0; + for (; tick < 2000 && player.alive; ++tick) tickCombat(player, effects); + CHECK(!player.alive); + CHECK_NEAR(player.health, 0.0f, 0.001f); + CHECK(tick < 500); +} + +VF_TEST(damageTypeClassificationMatchesTheMitigationRules) { + CHECK(bypassesArmor(DamageType::Void)); + CHECK(bypassesArmor(DamageType::Starve)); + CHECK(bypassesArmor(DamageType::Suffocation)); + CHECK(!bypassesArmor(DamageType::Explosion)); + CHECK(bypassesInvulnerability(DamageType::Void)); + CHECK(!bypassesInvulnerability(DamageType::Fire)); + CHECK(bypassesMitigation(DamageType::Void)); + CHECK(isFireDamage(DamageType::Lava)); + CHECK(isProjectileDamage(DamageType::Projectile)); + CHECK(environmentSource(DamageType::Fire).fiery); + CHECK(!environmentSource(DamageType::Fall).fiery); + CHECK(projectileSource(9, 3, {}, true).ownerId == 3); +}