From ebff0be679e122566f77795bf849492837815f93 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 17:29:24 +0200 Subject: [PATCH] test: cover ticksUntilDead --- tests/testEffect.cpp | 222 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 tests/testEffect.cpp diff --git a/tests/testEffect.cpp b/tests/testEffect.cpp new file mode 100644 index 0000000..b8d45ad --- /dev/null +++ b/tests/testEffect.cpp @@ -0,0 +1,222 @@ +#include "testHarness.h" +#include "vf/Effect.h" + +using namespace vf; + +namespace { + +int ticksUntilDead(EffectSet& set, float& health, float maxHealth, int limit) { + for (int tick = 0; tick < limit; ++tick) { + EffectSet::Pending pending = tickEffects(set); + applyEffectTick(pending, health, maxHealth); + if (health <= 0.0f) return tick + 1; + } + return -1; +} + +} // namespace + +VF_TEST(strongerEffectReplacesWeakerAndLongerOneOfEqualLevelWins) { + EffectSet set; + CHECK(applyEffect(set, makeEffect(EffectKind::Speed, 2, 100))); + CHECK_EQ(effectLevel(set, EffectKind::Speed), 2); + + CHECK(!applyEffect(set, makeEffect(EffectKind::Speed, 1, 5000))); + CHECK_EQ(effectLevel(set, EffectKind::Speed), 2); + CHECK_EQ(effectDuration(set, EffectKind::Speed), 100); + + CHECK(applyEffect(set, makeEffect(EffectKind::Speed, 2, 240))); + CHECK_EQ(effectDuration(set, EffectKind::Speed), 240); + + CHECK(applyEffect(set, makeEffect(EffectKind::Speed, 3, 10))); + CHECK_EQ(effectLevel(set, EffectKind::Speed), 3); + CHECK_EQ(effectDuration(set, EffectKind::Speed), 10); + CHECK_EQ(static_cast(set.active.size()), 1); +} + +VF_TEST(regenerationHealsOnItsIntervalAndFasterAtHigherLevel) { + EffectSet slow; + applyEffect(slow, makeEffect(EffectKind::Regeneration, 1, 500)); + float slowHealth = 10.0f; + for (int tick = 0; tick < 500; ++tick) { + EffectSet::Pending pending = tickEffects(slow); + applyEffectTick(pending, slowHealth, 20.0f); + } + CHECK_NEAR(slowHealth, 20.0f, 0.001f); + + EffectSet fast; + applyEffect(fast, makeEffect(EffectKind::Regeneration, 2, 100)); + float fastHealth = 10.0f; + int fastHeals = 0; + for (int tick = 0; tick < 100; ++tick) { + EffectSet::Pending pending = tickEffects(fast); + if (pending.regenerationHeal > 0.0f) ++fastHeals; + applyEffectTick(pending, fastHealth, 20.0f); + } + CHECK_EQ(periodicIntervalTicks(EffectKind::Regeneration, 0), 50); + CHECK_EQ(periodicIntervalTicks(EffectKind::Regeneration, 1), 25); + CHECK_EQ(fastHeals, 4); + CHECK_NEAR(fastHealth, 14.0f, 0.001f); +} + +VF_TEST(poisonNeverTakesTheLastHalfHeartButWitherKills) { + EffectSet poisoned; + applyEffect(poisoned, makeEffect(EffectKind::Poison, 1, 2000)); + float poisonedHealth = 20.0f; + CHECK_EQ(ticksUntilDead(poisoned, poisonedHealth, 20.0f, 2000), -1); + CHECK_NEAR(poisonedHealth, 1.0f, 0.001f); + + EffectSet withered; + applyEffect(withered, makeEffect(EffectKind::Wither, 1, 2000)); + float witheredHealth = 20.0f; + int deathTick = ticksUntilDead(withered, witheredHealth, 20.0f, 2000); + CHECK(deathTick > 0); + CHECK_NEAR(witheredHealth, 0.0f, 0.001f); +} + +VF_TEST(witherAtHigherLevelKillsFasterThanAtLevelOne) { + EffectSet weak; + applyEffect(weak, makeEffect(EffectKind::Wither, 1, 4000)); + float weakHealth = 20.0f; + int slowDeath = ticksUntilDead(weak, weakHealth, 20.0f, 4000); + + EffectSet strong; + applyEffect(strong, makeEffect(EffectKind::Wither, 3, 4000)); + float strongHealth = 20.0f; + int fastDeath = ticksUntilDead(strong, strongHealth, 20.0f, 4000); + + CHECK(slowDeath > 0); + CHECK(fastDeath > 0); + CHECK(fastDeath * 3 < slowDeath); +} + +VF_TEST(effectsExpireAfterTheirDurationAndStopModifyingAttributes) { + EffectSet set; + applyEffect(set, makeEffect(EffectKind::Speed, 3, 40)); + CHECK_NEAR(effectModifiers(set).moveSpeedMultiplier, 1.6f, 0.001f); + + int expired = 0; + for (int tick = 0; tick < 39; ++tick) expired += tickEffects(set).expiredCount; + CHECK_EQ(expired, 0); + CHECK(hasEffect(set, EffectKind::Speed)); + + expired += tickEffects(set).expiredCount; + CHECK_EQ(expired, 1); + CHECK(!hasEffect(set, EffectKind::Speed)); + CHECK_NEAR(effectModifiers(set).moveSpeedMultiplier, 1.0f, 0.001f); +} + +VF_TEST(speedAndSlownessMultiplyMovementInOppositeDirections) { + EffectSet hasted; + applyEffect(hasted, makeEffect(EffectKind::Speed, 2, 100)); + CHECK_NEAR(effectModifiers(hasted).moveSpeedMultiplier, 1.4f, 0.001f); + + EffectSet slowed; + applyEffect(slowed, makeEffect(EffectKind::Slowness, 4, 100)); + CHECK_NEAR(effectModifiers(slowed).moveSpeedMultiplier, 0.4f, 0.001f); + + EffectSet both; + applyEffect(both, makeEffect(EffectKind::Speed, 2, 100)); + applyEffect(both, makeEffect(EffectKind::Slowness, 2, 100)); + CHECK_NEAR(effectModifiers(both).moveSpeedMultiplier, 1.4f * 0.7f, 0.001f); +} + +VF_TEST(miningFatigueCompoundsWithHasteOnTheMiningMultiplier) { + EffectSet set; + applyEffect(set, makeEffect(EffectKind::Haste, 2, 200)); + CHECK_NEAR(effectModifiers(set).miningSpeedMultiplier, 1.4f, 0.001f); + + applyEffect(set, makeEffect(EffectKind::MiningFatigue, 1, 200)); + CHECK_NEAR(effectModifiers(set).miningSpeedMultiplier, 1.4f * 0.3f, 0.001f); +} + +VF_TEST(instantHealthResolvesOnTheNextTickAndLeavesNoResidue) { + EffectSet set; + applyEffect(set, makeEffect(EffectKind::InstantHealth, 2, 0)); + CHECK_EQ(static_cast(set.active.size()), 1); + + float health = 6.0f; + EffectSet::Pending pending = tickEffects(set); + CHECK_NEAR(pending.instantHeal, 8.0f, 0.001f); + applyEffectTick(pending, health, 20.0f); + CHECK_NEAR(health, 14.0f, 0.001f); + CHECK_EQ(static_cast(set.active.size()), 0); + + pending = tickEffects(set); + CHECK(pending.empty()); +} + +VF_TEST(instantDamageAtReducedPotencyHitsSofterThanADirectDose) { + EffectSet full; + EffectInstance strong = makeEffect(EffectKind::InstantDamage, 1, 0); + applyEffect(full, strong); + float fullHealth = 20.0f; + applyEffectTick(tickEffects(full), fullHealth, 20.0f); + CHECK_NEAR(fullHealth, 14.0f, 0.001f); + + EffectSet weak; + EffectInstance grazed = strong; + grazed.potency = 0.5f; + applyEffect(weak, grazed); + float weakHealth = 20.0f; + applyEffectTick(tickEffects(weak), weakHealth, 20.0f); + CHECK_NEAR(weakHealth, 17.0f, 0.001f); +} + +VF_TEST(fermentedSpiderEyeInversionMapsEffectsToTheirOpposites) { + CHECK(invertedEffect(EffectKind::Speed) == EffectKind::Slowness); + CHECK(invertedEffect(EffectKind::InstantHealth) == EffectKind::InstantDamage); + CHECK(invertedEffect(EffectKind::Poison) == EffectKind::InstantDamage); + CHECK(invertedEffect(EffectKind::NightVision) == EffectKind::Invisibility); + CHECK(invertedEffect(EffectKind::Regeneration) == EffectKind::Count); + CHECK(isHarmfulEffect(EffectKind::Wither)); + CHECK(!isHarmfulEffect(EffectKind::Regeneration)); + CHECK(isInstantEffect(EffectKind::InstantDamage)); +} + +VF_TEST(absorptionAndHealthBoostRaiseTheCeilingThroughModifiers) { + EffectSet set; + applyEffect(set, makeEffect(EffectKind::HealthBoost, 2, 400)); + applyEffect(set, makeEffect(EffectKind::Absorption, 3, 400)); + EffectModifier modifier = effectModifiers(set); + CHECK_NEAR(modifier.maxHealthBonus, 8.0f, 0.001f); + CHECK_NEAR(modifier.absorptionHearts, 12.0f, 0.001f); + + applyEffect(set, makeEffect(EffectKind::FireResistance, 1, 400)); + applyEffect(set, makeEffect(EffectKind::NightVision, 1, 400)); + modifier = effectModifiers(set); + CHECK(modifier.fireImmune); + CHECK(modifier.nightVision); + CHECK(!modifier.waterBreathing); +} + +VF_TEST(regenerationAndPoisonTogetherLeaveHealthDrifting) { + EffectSet set; + applyEffect(set, makeEffect(EffectKind::Regeneration, 1, 600)); + applyEffect(set, makeEffect(EffectKind::Poison, 1, 600)); + + float health = 10.0f; + for (int tick = 0; tick < 600; ++tick) { + EffectSet::Pending pending = tickEffects(set); + applyEffectTick(pending, health, 20.0f); + } + // Poison fires twice as often as regeneration, so the net drift is down, + // and it still stops at the last half heart. + CHECK_NEAR(health, 1.0f, 0.001f); + CHECK_EQ(static_cast(set.active.size()), 0); +} + +VF_TEST(removingAndClearingEffectsDropsThemImmediately) { + EffectSet set; + applyEffect(set, makeEffect(EffectKind::Strength, 2, 300)); + applyEffect(set, makeEffect(EffectKind::Weakness, 1, 300)); + CHECK_NEAR(effectModifiers(set).attackDamageBonus, 2.0f, 0.001f); + + CHECK(removeEffect(set, EffectKind::Weakness)); + CHECK(!removeEffect(set, EffectKind::Weakness)); + CHECK_NEAR(effectModifiers(set).attackDamageBonus, 6.0f, 0.001f); + + clearEffects(set); + CHECK_EQ(static_cast(set.active.size()), 0); + CHECK_NEAR(effectModifiers(set).attackDamageBonus, 0.0f, 0.001f); +}