410 lines
14 KiB
C++
410 lines
14 KiB
C++
#include "vf/Damage.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace vf {
|
|
|
|
namespace {
|
|
|
|
uint32_t xorshift(uint32_t& state) {
|
|
state ^= state << 13;
|
|
state ^= state >> 17;
|
|
state ^= state << 5;
|
|
return state;
|
|
}
|
|
|
|
float unitFloat(uint32_t& rng) { return static_cast<float>(xorshift(rng)) / 4294967295.0f; }
|
|
|
|
int rollBelow(uint32_t& rng, int bound) {
|
|
if (bound <= 1) return 0;
|
|
return static_cast<int>(xorshift(rng) % static_cast<uint32_t>(bound));
|
|
}
|
|
|
|
// Vanilla per-slot durability bases, scaled by the material factor below.
|
|
int slotDurabilityBase(Armor::Slot slot) {
|
|
switch (slot) {
|
|
case Armor::Slot::Helmet: return 11;
|
|
case Armor::Slot::Chestplate: return 16;
|
|
case Armor::Slot::Leggings: return 15;
|
|
case Armor::Slot::Boots: return 13;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
int materialDurabilityFactor(Armor::Material material) {
|
|
switch (material) {
|
|
case Armor::Material::Leather: return 5;
|
|
case Armor::Material::Chain: return 15;
|
|
case Armor::Material::Iron: return 15;
|
|
case Armor::Material::Gold: return 7;
|
|
case Armor::Material::Diamond: return 33;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
int protectionPointsFor(const EnchantSet& enchants, DamageType type) {
|
|
int points = 0;
|
|
int general = enchants.levelOf(EnchantKind::Protection);
|
|
if (general > 0 && type != DamageType::Void && type != DamageType::Starve) points += general;
|
|
if (isFireDamage(type)) points += 2 * enchants.levelOf(EnchantKind::FireProtection);
|
|
if (type == DamageType::Explosion) points += 2 * enchants.levelOf(EnchantKind::BlastProtection);
|
|
if (isProjectileDamage(type)) {
|
|
points += 2 * enchants.levelOf(EnchantKind::ProjectileProtection);
|
|
}
|
|
if (type == DamageType::Fall) points += 3 * enchants.levelOf(EnchantKind::FeatherFalling);
|
|
return points;
|
|
}
|
|
|
|
bool unbreakingBlocksWear(int unbreakingLevel, uint32_t& rng) {
|
|
if (unbreakingLevel <= 0) return false;
|
|
float ignoreChance = 0.6f + 0.4f / static_cast<float>(unbreakingLevel + 1);
|
|
return unitFloat(rng) < ignoreChance;
|
|
}
|
|
|
|
Vec3 knockbackAlong(const Vec3& heading, float strength) {
|
|
Vec3 flat = normalize(Vec3{heading.x, 0.0f, heading.z});
|
|
if (length(flat) <= 0.0f) flat = {1.0f, 0.0f, 0.0f};
|
|
float horizontal = 0.4f + strength * 0.5f;
|
|
return {flat.x * horizontal, 0.4f, flat.z * horizontal};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool bypassesArmor(DamageType type) {
|
|
switch (type) {
|
|
case DamageType::Void:
|
|
case DamageType::Starve:
|
|
case DamageType::Suffocation:
|
|
case DamageType::Magic:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool bypassesInvulnerability(DamageType type) {
|
|
return type == DamageType::Void || type == DamageType::Starve;
|
|
}
|
|
|
|
bool bypassesMitigation(DamageType type) { return type == DamageType::Void; }
|
|
|
|
bool isFireDamage(DamageType type) {
|
|
return type == DamageType::Fire || type == DamageType::Lava;
|
|
}
|
|
|
|
bool isProjectileDamage(DamageType type) { return type == DamageType::Projectile; }
|
|
|
|
HurtSource meleeSource(int attackerId, const Vec3& origin, bool byPlayer) {
|
|
HurtSource source;
|
|
source.type = DamageType::Melee;
|
|
source.origin = origin;
|
|
source.attackerId = attackerId;
|
|
source.ownerId = attackerId;
|
|
source.byPlayer = byPlayer;
|
|
return source;
|
|
}
|
|
|
|
HurtSource projectileSource(int projectileId, int ownerId, const Vec3& origin, bool byPlayer) {
|
|
HurtSource source;
|
|
source.type = DamageType::Projectile;
|
|
source.origin = origin;
|
|
source.attackerId = projectileId;
|
|
source.ownerId = ownerId;
|
|
source.byPlayer = byPlayer;
|
|
return source;
|
|
}
|
|
|
|
HurtSource environmentSource(DamageType type, const Vec3& origin) {
|
|
HurtSource source;
|
|
source.type = type;
|
|
source.origin = origin;
|
|
source.fiery = isFireDamage(type);
|
|
return source;
|
|
}
|
|
|
|
Armor makeArmor(Armor::Material material, Armor::Slot slot) {
|
|
Armor piece;
|
|
piece.material = material;
|
|
piece.slot = slot;
|
|
piece.maxDurability = slotDurabilityBase(slot) * materialDurabilityFactor(material);
|
|
piece.durability = piece.maxDurability;
|
|
return piece;
|
|
}
|
|
|
|
int armorPoints(const Armor& piece) {
|
|
if (!piece.worn()) return 0;
|
|
switch (piece.material) {
|
|
case Armor::Material::Leather:
|
|
switch (piece.slot) {
|
|
case Armor::Slot::Helmet: return 1;
|
|
case Armor::Slot::Chestplate: return 3;
|
|
case Armor::Slot::Leggings: return 2;
|
|
default: return 1;
|
|
}
|
|
case Armor::Material::Chain:
|
|
switch (piece.slot) {
|
|
case Armor::Slot::Helmet: return 2;
|
|
case Armor::Slot::Chestplate: return 5;
|
|
case Armor::Slot::Leggings: return 4;
|
|
default: return 1;
|
|
}
|
|
case Armor::Material::Iron:
|
|
switch (piece.slot) {
|
|
case Armor::Slot::Helmet: return 2;
|
|
case Armor::Slot::Chestplate: return 6;
|
|
case Armor::Slot::Leggings: return 5;
|
|
default: return 2;
|
|
}
|
|
case Armor::Material::Gold:
|
|
switch (piece.slot) {
|
|
case Armor::Slot::Helmet: return 2;
|
|
case Armor::Slot::Chestplate: return 5;
|
|
case Armor::Slot::Leggings: return 3;
|
|
default: return 1;
|
|
}
|
|
case Armor::Material::Diamond:
|
|
switch (piece.slot) {
|
|
case Armor::Slot::Helmet: return 3;
|
|
case Armor::Slot::Chestplate: return 8;
|
|
case Armor::Slot::Leggings: return 6;
|
|
default: return 3;
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
float armorToughness(const Armor& piece) {
|
|
if (!piece.worn()) return 0.0f;
|
|
return piece.material == Armor::Material::Diamond ? 2.0f : 0.0f;
|
|
}
|
|
|
|
int armorEnchantability(Armor::Material material) {
|
|
switch (material) {
|
|
case Armor::Material::Leather: return 15;
|
|
case Armor::Material::Chain: return 12;
|
|
case Armor::Material::Iron: return 9;
|
|
case Armor::Material::Gold: return 25;
|
|
case Armor::Material::Diamond: return 10;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
Enchantment::Target armorTarget(Armor::Slot slot) {
|
|
switch (slot) {
|
|
case Armor::Slot::Helmet: return Enchantment::Target::Helmet;
|
|
case Armor::Slot::Chestplate: return Enchantment::Target::Chestplate;
|
|
case Armor::Slot::Leggings: return Enchantment::Target::Leggings;
|
|
default: return Enchantment::Target::Boots;
|
|
}
|
|
}
|
|
|
|
int ArmorSet::totalPoints() const {
|
|
int total = 0;
|
|
for (const Armor& piece : pieces) total += armorPoints(piece);
|
|
return total;
|
|
}
|
|
|
|
float ArmorSet::totalToughness() const {
|
|
float total = 0.0f;
|
|
for (const Armor& piece : pieces) total += armorToughness(piece);
|
|
return total;
|
|
}
|
|
|
|
int ArmorSet::protectionPoints(DamageType type) const {
|
|
int total = 0;
|
|
for (const Armor& piece : pieces) {
|
|
if (!piece.worn()) continue;
|
|
total += protectionPointsFor(piece.enchants, type);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
int ArmorSet::levelOf(EnchantKind kind) const {
|
|
int best = 0;
|
|
for (const Armor& piece : pieces) {
|
|
if (!piece.worn()) continue;
|
|
best = std::max(best, piece.enchants.levelOf(kind));
|
|
}
|
|
return best;
|
|
}
|
|
|
|
float armorReduction(int points, float toughness, float damage) {
|
|
if (points <= 0) return damage;
|
|
float pointsF = static_cast<float>(points);
|
|
float defence = std::max(pointsF / 5.0f, pointsF - damage / (2.0f + toughness / 4.0f));
|
|
defence = std::min(20.0f, defence);
|
|
return damage * (1.0f - defence / 25.0f);
|
|
}
|
|
|
|
float protectionFactor(int points) {
|
|
int capped = std::min(points, 20);
|
|
if (capped <= 0) return 1.0f;
|
|
return 1.0f - static_cast<float>(capped) * 0.04f;
|
|
}
|
|
|
|
float effectiveMaxHealth(const CombatStats& stats, const EffectSet& effects) {
|
|
return stats.baseMaxHealth + effectModifiers(effects).maxHealthBonus;
|
|
}
|
|
|
|
int attackCooldownTicks(float attackSpeed) {
|
|
if (attackSpeed <= 0.0f) return ticksPerSecond;
|
|
int ticks = static_cast<int>(std::lround(static_cast<float>(ticksPerSecond) / attackSpeed));
|
|
return std::max(1, ticks);
|
|
}
|
|
|
|
float attackCharge(const CombatStats& stats) {
|
|
int cooldown = attackCooldownTicks(stats.attackSpeed);
|
|
float charge = static_cast<float>(stats.attackChargeTicks) / static_cast<float>(cooldown);
|
|
return std::min(1.0f, std::max(0.0f, charge));
|
|
}
|
|
|
|
void registerSwing(CombatStats& stats) { stats.attackChargeTicks = 0; }
|
|
|
|
EffectSet::Pending tickCombat(CombatStats& stats, EffectSet& effects) {
|
|
if (stats.invulnerableTicks > 0) --stats.invulnerableTicks;
|
|
if (stats.invulnerableTicks == 0) stats.lastDamageTaken = 0.0f;
|
|
|
|
int cooldown = attackCooldownTicks(stats.attackSpeed);
|
|
if (stats.attackChargeTicks < cooldown) ++stats.attackChargeTicks;
|
|
|
|
EffectSet::Pending pending = tickEffects(effects);
|
|
EffectModifier modifier = effectModifiers(effects);
|
|
stats.absorption = std::max(stats.absorption, modifier.absorptionHearts);
|
|
|
|
float maxHealth = stats.baseMaxHealth + modifier.maxHealthBonus;
|
|
applyEffectTick(pending, stats.health, maxHealth);
|
|
if (stats.health <= 0.0f) {
|
|
stats.health = 0.0f;
|
|
stats.alive = false;
|
|
}
|
|
return pending;
|
|
}
|
|
|
|
float meleeAttackDamage(const CombatStats& attacker, const EnchantSet& weapon,
|
|
const EffectSet& attackerEffects, bool undeadTarget, bool arthropodTarget,
|
|
bool critical) {
|
|
float charge = attackCharge(attacker);
|
|
float damage = attacker.baseAttackDamage * (0.2f + charge * charge * 0.8f);
|
|
damage += effectModifiers(attackerEffects).attackDamageBonus;
|
|
damage = std::max(0.0f, damage);
|
|
|
|
float enchantBonus = 0.0f;
|
|
int sharpness = weapon.levelOf(EnchantKind::Sharpness);
|
|
if (sharpness > 0) enchantBonus += 1.0f + 0.5f * static_cast<float>(sharpness - 1);
|
|
if (undeadTarget) enchantBonus += 2.5f * static_cast<float>(weapon.levelOf(EnchantKind::Smite));
|
|
if (arthropodTarget) {
|
|
enchantBonus += 2.5f * static_cast<float>(weapon.levelOf(EnchantKind::BaneOfArthropods));
|
|
}
|
|
// A partially charged swing only lands a fraction of the enchantment bonus.
|
|
damage += enchantBonus * charge;
|
|
|
|
if (critical && charge >= 0.9f) damage *= criticalMultiplier;
|
|
return damage;
|
|
}
|
|
|
|
float fallDamage(float fallDistance, const EffectSet& effects) {
|
|
float safeDistance = 3.0f + static_cast<float>(effectLevel(effects, EffectKind::JumpBoost));
|
|
float excess = fallDistance - safeDistance;
|
|
if (excess <= 0.0f) return 0.0f;
|
|
return std::floor(excess);
|
|
}
|
|
|
|
Vec3 knockbackVector(const Vec3& attackerPos, const Vec3& targetPos, float strength) {
|
|
return knockbackAlong(targetPos - attackerPos, strength);
|
|
}
|
|
|
|
int wearArmor(ArmorSet& armor, float incomingDamage, uint32_t& rng) {
|
|
int wear = std::max(1, static_cast<int>(std::floor(incomingDamage / 4.0f)));
|
|
int spent = 0;
|
|
for (Armor& piece : armor.pieces) {
|
|
if (!piece.worn()) continue;
|
|
int unbreaking = piece.enchants.levelOf(EnchantKind::Unbreaking);
|
|
int applied = 0;
|
|
for (int i = 0; i < wear; ++i) {
|
|
if (unbreakingBlocksWear(unbreaking, rng)) continue;
|
|
++applied;
|
|
}
|
|
applied = std::min(applied, piece.durability);
|
|
piece.durability -= applied;
|
|
spent += applied;
|
|
if (piece.durability <= 0) piece = Armor{};
|
|
}
|
|
return spent;
|
|
}
|
|
|
|
CombatResult applyDamage(CombatStats& target, const EffectSet& targetEffects,
|
|
const DamageEvent& event, uint32_t& rng) {
|
|
CombatResult result;
|
|
result.incoming = event.amount;
|
|
if (!target.alive || event.amount <= 0.0f) return result;
|
|
|
|
DamageType type = event.source.type;
|
|
EffectModifier modifier = effectModifiers(targetEffects);
|
|
if (modifier.fireImmune && isFireDamage(type)) return result;
|
|
|
|
float raw = event.amount;
|
|
if (!bypassesInvulnerability(type)) {
|
|
if (target.invulnerableTicks > 0) {
|
|
if (raw <= target.lastDamageTaken) {
|
|
result.blockedByInvulnerability = true;
|
|
result.applied = false;
|
|
return result;
|
|
}
|
|
raw -= target.lastDamageTaken;
|
|
target.lastDamageTaken = event.amount;
|
|
} else {
|
|
target.lastDamageTaken = event.amount;
|
|
target.invulnerableTicks = invulnerabilityTicks;
|
|
}
|
|
}
|
|
|
|
float afterArmor = raw;
|
|
if (!bypassesArmor(type)) {
|
|
afterArmor = armorReduction(target.armor.totalPoints(), target.armor.totalToughness(), raw);
|
|
}
|
|
result.afterArmor = afterArmor;
|
|
|
|
float afterResistance = afterArmor;
|
|
float afterProtection = afterArmor;
|
|
if (!bypassesMitigation(type)) {
|
|
afterResistance = afterArmor * modifier.incomingDamageMultiplier;
|
|
afterProtection = afterResistance * protectionFactor(target.armor.protectionPoints(type));
|
|
}
|
|
result.afterResistance = afterResistance;
|
|
result.afterProtection = afterProtection;
|
|
|
|
float remaining = afterProtection;
|
|
if (target.absorption > 0.0f) {
|
|
float absorbed = std::min(target.absorption, remaining);
|
|
target.absorption -= absorbed;
|
|
remaining -= absorbed;
|
|
result.absorbedByAbsorption = absorbed;
|
|
}
|
|
|
|
float before = target.health;
|
|
target.health = std::max(0.0f, target.health - remaining);
|
|
result.healthLost = before - target.health;
|
|
result.applied = true;
|
|
result.fatal = target.health <= 0.0f;
|
|
if (result.fatal) target.alive = false;
|
|
|
|
if (!bypassesArmor(type) && afterProtection > 0.0f) {
|
|
result.armorDurabilityLost = wearArmor(target.armor, raw, rng);
|
|
}
|
|
|
|
if (type == DamageType::Melee || type == DamageType::Projectile) {
|
|
result.knockback = knockbackAlong(event.direction, event.knockbackStrength);
|
|
int thorns = target.armor.levelOf(EnchantKind::Thorns);
|
|
if (thorns > 0 && event.source.attackerId >= 0) {
|
|
if (rollBelow(rng, 100) < thorns * 15) {
|
|
result.thornsDamage = static_cast<float>(1 + rollBelow(rng, 4));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace vf
|