384 lines
13 KiB
C++
384 lines
13 KiB
C++
#include "testHarness.h"
|
|
#include "vf/Damage.h"
|
|
|
|
using namespace vf;
|
|
|
|
namespace {
|
|
|
|
ArmorSet fullSet(Armor::Material material) {
|
|
ArmorSet set;
|
|
set.equip(makeArmor(material, Armor::Slot::Helmet));
|
|
set.equip(makeArmor(material, Armor::Slot::Chestplate));
|
|
set.equip(makeArmor(material, Armor::Slot::Leggings));
|
|
set.equip(makeArmor(material, Armor::Slot::Boots));
|
|
return set;
|
|
}
|
|
|
|
void enchantWholeSet(ArmorSet& set, EnchantKind kind, int level) {
|
|
for (Armor& piece : set.pieces) addEnchant(piece.enchants, kind, level);
|
|
}
|
|
|
|
DamageEvent hitFor(DamageType type, float amount) {
|
|
DamageEvent event;
|
|
event.source = environmentSource(type);
|
|
event.amount = amount;
|
|
return event;
|
|
}
|
|
|
|
void idleTicks(CombatStats& stats, EffectSet& effects, int count) {
|
|
for (int i = 0; i < count; ++i) tickCombat(stats, effects);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
VF_TEST(diamondArmorReducesMeleeDamageByTheVanillaCurve) {
|
|
ArmorSet diamond = fullSet(Armor::Material::Diamond);
|
|
CHECK_EQ(diamond.totalPoints(), 20);
|
|
CHECK_NEAR(diamond.totalToughness(), 8.0f, 0.001f);
|
|
CHECK_NEAR(armorReduction(20, 8.0f, 10.0f), 3.0f, 0.001f);
|
|
|
|
ArmorSet leather = fullSet(Armor::Material::Leather);
|
|
CHECK_EQ(leather.totalPoints(), 7);
|
|
CHECK_NEAR(leather.totalToughness(), 0.0f, 0.001f);
|
|
// Weak armour hits the points/5 floor against a big blow.
|
|
CHECK_NEAR(armorReduction(7, 0.0f, 20.0f), 20.0f * (1.0f - 1.4f / 25.0f), 0.001f);
|
|
}
|
|
|
|
VF_TEST(voidAndStarveDamageIgnoreArmorEntirely) {
|
|
EffectSet none;
|
|
uint32_t rng = 7u;
|
|
CombatStats player;
|
|
player.armor = fullSet(Armor::Material::Diamond);
|
|
|
|
CombatResult voided = applyDamage(player, none, hitFor(DamageType::Void, 6.0f), rng);
|
|
CHECK(voided.applied);
|
|
CHECK_NEAR(voided.healthLost, 6.0f, 0.001f);
|
|
CHECK_EQ(voided.armorDurabilityLost, 0);
|
|
|
|
player.health = 20.0f;
|
|
CombatResult melee = applyDamage(player, none, hitFor(DamageType::Melee, 6.0f), rng);
|
|
CHECK(melee.applied);
|
|
CHECK(melee.healthLost < 3.0f);
|
|
CHECK(melee.armorDurabilityLost > 0);
|
|
}
|
|
|
|
VF_TEST(invulnerabilityWindowSwallowsWeakerFollowUpsButNotStrongerOnes) {
|
|
EffectSet none;
|
|
uint32_t rng = 11u;
|
|
CombatStats player;
|
|
|
|
CombatResult first = applyDamage(player, none, hitFor(DamageType::Melee, 5.0f), rng);
|
|
CHECK(first.applied);
|
|
CHECK_NEAR(player.health, 15.0f, 0.001f);
|
|
CHECK_EQ(player.invulnerableTicks, invulnerabilityTicks);
|
|
|
|
CombatResult weaker = applyDamage(player, none, hitFor(DamageType::Melee, 3.0f), rng);
|
|
CHECK(weaker.blockedByInvulnerability);
|
|
CHECK(!weaker.applied);
|
|
CHECK_NEAR(player.health, 15.0f, 0.001f);
|
|
|
|
CombatResult stronger = applyDamage(player, none, hitFor(DamageType::Melee, 8.0f), rng);
|
|
CHECK(stronger.applied);
|
|
CHECK_NEAR(player.health, 12.0f, 0.001f);
|
|
|
|
idleTicks(player, none, invulnerabilityTicks);
|
|
CHECK_EQ(player.invulnerableTicks, 0);
|
|
CombatResult fresh = applyDamage(player, none, hitFor(DamageType::Melee, 2.0f), rng);
|
|
CHECK(fresh.applied);
|
|
CHECK_NEAR(player.health, 10.0f, 0.001f);
|
|
}
|
|
|
|
VF_TEST(starvationLandsEveryTickBecauseItIgnoresTheInvulnerabilityWindow) {
|
|
EffectSet none;
|
|
uint32_t rng = 3u;
|
|
CombatStats player;
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
CombatResult starve = applyDamage(player, none, hitFor(DamageType::Starve, 1.0f), rng);
|
|
CHECK(starve.applied);
|
|
}
|
|
CHECK_NEAR(player.health, 15.0f, 0.001f);
|
|
}
|
|
|
|
VF_TEST(attackChargeRampsBackToFullAndScalesSwingDamage) {
|
|
EffectSet none;
|
|
CombatStats attacker;
|
|
attacker.baseAttackDamage = 7.0f;
|
|
attacker.attackSpeed = 4.0f;
|
|
CHECK_EQ(attackCooldownTicks(4.0f), 5);
|
|
|
|
registerSwing(attacker);
|
|
CHECK_NEAR(attackCharge(attacker), 0.0f, 0.001f);
|
|
EnchantSet bare;
|
|
float coldSwing = meleeAttackDamage(attacker, bare, none, false, false, false);
|
|
CHECK_NEAR(coldSwing, 7.0f * 0.2f, 0.001f);
|
|
|
|
for (int i = 0; i < 5; ++i) tickCombat(attacker, none);
|
|
CHECK_NEAR(attackCharge(attacker), 1.0f, 0.001f);
|
|
float fullSwing = meleeAttackDamage(attacker, bare, none, false, false, false);
|
|
CHECK_NEAR(fullSwing, 7.0f, 0.001f);
|
|
|
|
// The bar caps, extra ticks change nothing.
|
|
for (int i = 0; i < 20; ++i) tickCombat(attacker, none);
|
|
CHECK_NEAR(attackCharge(attacker), 1.0f, 0.001f);
|
|
}
|
|
|
|
VF_TEST(sharpnessAndCriticalHitsStackOnTopOfBaseSwingDamage) {
|
|
EffectSet none;
|
|
CombatStats attacker;
|
|
attacker.baseAttackDamage = 7.0f;
|
|
attacker.attackChargeTicks = 5;
|
|
|
|
EnchantSet plain;
|
|
EnchantSet sharp;
|
|
CHECK(addEnchant(sharp, EnchantKind::Sharpness, 5));
|
|
|
|
CHECK_NEAR(meleeAttackDamage(attacker, plain, none, false, false, false), 7.0f, 0.001f);
|
|
CHECK_NEAR(meleeAttackDamage(attacker, sharp, none, false, false, false), 10.0f, 0.001f);
|
|
CHECK_NEAR(meleeAttackDamage(attacker, sharp, none, false, false, true), 15.0f, 0.001f);
|
|
|
|
EnchantSet smite;
|
|
CHECK(addEnchant(smite, EnchantKind::Smite, 4));
|
|
CHECK_NEAR(meleeAttackDamage(attacker, smite, none, false, false, false), 7.0f, 0.001f);
|
|
CHECK_NEAR(meleeAttackDamage(attacker, smite, none, true, false, false), 17.0f, 0.001f);
|
|
}
|
|
|
|
VF_TEST(strengthAddsAndWeaknessRemovesFlatMeleeDamage) {
|
|
CombatStats attacker;
|
|
attacker.baseAttackDamage = 7.0f;
|
|
attacker.attackChargeTicks = 5;
|
|
EnchantSet bare;
|
|
|
|
EffectSet strong;
|
|
applyEffect(strong, makeEffect(EffectKind::Strength, 2, 200));
|
|
CHECK_NEAR(meleeAttackDamage(attacker, bare, strong, false, false, false), 13.0f, 0.001f);
|
|
|
|
EffectSet weak;
|
|
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);
|
|
}
|