use crate::comp::buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource}; #[cfg(not(target_arch = "wasm32"))] use crate::{ comp::{ inventory::{ item::{ armor::Protection, tool::{self, Tool, ToolKind}, Item, ItemDesc, ItemKind, MaterialStatManifest, }, slot::EquipSlot, }, skillset::SkillGroupKind, Alignment, Body, CharacterState, Combo, Energy, Health, HealthChange, Inventory, Ori, Player, Poise, PoiseChange, SkillSet, Stats, }, event::ServerEvent, outcome::Outcome, states::utils::StageSection, uid::{Uid, UidAllocator}, util::Dir, }; #[cfg(not(target_arch = "wasm32"))] use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; use crate::{comp::Group, resources::Time}; #[cfg(not(target_arch = "wasm32"))] use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage}; #[cfg(not(target_arch = "wasm32"))] use std::{ops::MulAssign, time::Duration}; #[cfg(not(target_arch = "wasm32"))] use vek::*; #[cfg(not(target_arch = "wasm32"))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum GroupTarget { InGroup, OutOfGroup, } #[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub enum AttackSource { Melee, Projectile, Beam, Shockwave, Explosion, } #[cfg(not(target_arch = "wasm32"))] #[derive(Copy, Clone)] pub struct AttackerInfo<'a> { pub entity: EcsEntity, pub uid: Uid, pub group: Option<&'a Group>, pub energy: Option<&'a Energy>, pub combo: Option<&'a Combo>, pub inventory: Option<&'a Inventory>, } #[cfg(not(target_arch = "wasm32"))] pub struct TargetInfo<'a> { pub entity: EcsEntity, pub uid: Uid, pub inventory: Option<&'a Inventory>, pub stats: Option<&'a Stats>, pub health: Option<&'a Health>, pub pos: Vec3, pub ori: Option<&'a Ori>, pub char_state: Option<&'a CharacterState>, pub energy: Option<&'a Energy>, } #[derive(Clone, Copy)] pub struct AttackOptions { pub target_dodging: bool, pub may_harm: bool, pub target_group: GroupTarget, } #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Yeet clone derive pub struct Attack { damages: Vec, effects: Vec, crit_chance: f32, crit_multiplier: f32, } #[cfg(not(target_arch = "wasm32"))] impl Default for Attack { fn default() -> Self { Self { damages: Vec::new(), effects: Vec::new(), crit_chance: 0.0, crit_multiplier: 1.0, } } } #[cfg(not(target_arch = "wasm32"))] impl Attack { #[must_use] pub fn with_damage(mut self, damage: AttackDamage) -> Self { self.damages.push(damage); self } #[must_use] pub fn with_effect(mut self, effect: AttackEffect) -> Self { self.effects.push(effect); self } #[must_use] pub fn with_crit(mut self, crit_chance: f32, crit_multiplier: f32) -> Self { self.crit_chance = crit_chance; self.crit_multiplier = crit_multiplier; self } #[must_use] pub fn with_combo_increment(self) -> Self { self.with_effect( AttackEffect::new(None, CombatEffect::Combo(1)) .with_requirement(CombatRequirement::AnyDamage), ) } pub fn effects(&self) -> impl Iterator { self.effects.iter() } pub fn compute_damage_reduction( target: &TargetInfo, source: AttackSource, dir: Dir, damage: Damage, mut emit: impl FnMut(ServerEvent), mut emit_outcome: impl FnMut(Outcome), ) -> f32 { let damage_reduction = Damage::compute_damage_reduction(Some(damage), target.inventory, target.stats); let block_reduction = match source { AttackSource::Melee => { if let (Some(CharacterState::BasicBlock(data)), Some(ori)) = (target.char_state, target.ori) { if ori.look_vec().angle_between(-*dir) < data.static_data.max_angle.to_radians() { let parry = matches!(data.stage_section, StageSection::Buildup); emit_outcome(Outcome::Block { parry, pos: target.pos, uid: target.uid, }); emit(ServerEvent::Parry { entity: target.entity, energy_cost: data.static_data.energy_cost, }); if parry { 1.0 } else { data.static_data.block_strength } } else { 0.0 } } else { 0.0 } }, _ => 0.0, }; 1.0 - (1.0 - damage_reduction) * (1.0 - block_reduction) } #[allow(clippy::too_many_arguments)] pub fn apply_attack( &self, attacker: Option, target: TargetInfo, dir: Dir, options: AttackOptions, // Currently strength_modifier just modifies damage, // maybe look into modifying strength of other effects? strength_modifier: f32, attack_source: AttackSource, time: Time, mut emit: impl FnMut(ServerEvent), mut emit_outcome: impl FnMut(Outcome), ) -> bool { let AttackOptions { target_dodging, may_harm, target_group, } = options; // target == OutOfGroup is basic heuristic that this // "attack" has negative effects. // // so if target dodges this "attack" or we don't want to harm target, // it should avoid such "damage" or effect let avoid_damage = |attack_damage: &AttackDamage| { matches!(attack_damage.target, Some(GroupTarget::OutOfGroup)) && (target_dodging || !may_harm) }; let avoid_effect = |attack_effect: &AttackEffect| { matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)) && (target_dodging || !may_harm) }; let is_crit = thread_rng().gen::() < self.crit_chance; let mut is_applied = false; let mut accumulated_damage = 0.0; for damage in self .damages .iter() .filter(|d| d.target.map_or(true, |t| t == target_group)) .filter(|d| !avoid_damage(d)) { is_applied = true; let damage_reduction = Attack::compute_damage_reduction( &target, attack_source, dir, damage.damage, &mut emit, &mut emit_outcome, ); let change = damage.damage.calculate_health_change( damage_reduction, attacker.map(|x| x.into()), is_crit, self.crit_multiplier, strength_modifier, time, ); let applied_damage = -change.amount; accumulated_damage += applied_damage; emit_outcome(Outcome::Damage { pos: target.pos }); if change.amount.abs() > Health::HEALTH_EPSILON { emit(ServerEvent::HealthChange { entity: target.entity, change, }); match damage.damage.kind { DamageKind::Slashing => { // For slashing damage, reduce target energy by some fraction of applied // damage. When target would lose more energy than they have, deal an // equivalent amount of damage if let Some(target_energy) = target.energy { let energy_change = applied_damage * SLASHING_ENERGY_FRACTION; if energy_change > target_energy.current() { let health_change = HealthChange { amount: -(energy_change - target_energy.current()), by: attacker.map(|x| x.into()), cause: Some(damage.damage.source), time, }; emit(ServerEvent::HealthChange { entity: target.entity, change: health_change, }); } emit(ServerEvent::EnergyChange { entity: target.entity, change: -energy_change, }); } }, DamageKind::Crushing => { // For crushing damage, reduce target poise by some fraction of the amount // of damage that was reduced by target's protection // Damage reduction should never equal 1 here as otherwise the check above // that health change amount is greater than 0 would fail. let reduced_damage = applied_damage * damage_reduction / (1.0 - damage_reduction); let poise = reduced_damage * CRUSHING_POISE_FRACTION; let change = -Poise::apply_poise_reduction(poise, target.inventory); let poise_change = PoiseChange { amount: change, impulse: *dir, by: attacker.map(|x| x.into()), cause: Some(damage.damage.source), time, }; if change.abs() > Poise::POISE_EPSILON { emit(ServerEvent::PoiseChange { entity: target.entity, change: poise_change, }); } }, // Piercing damage ignores some penetration, and is handled when damage // reduction is computed Energy is a placeholder damage type DamageKind::Piercing | DamageKind::Energy => {}, } for effect in damage.effects.iter() { match effect { CombatEffect::Knockback(kb) => { let impulse = kb.calculate_impulse(dir) * strength_modifier; if !impulse.is_approx_zero() { emit(ServerEvent::Knockback { entity: target.entity, impulse, }); } }, CombatEffect::EnergyReward(ec) => { if let Some(attacker) = attacker { emit(ServerEvent::EnergyChange { entity: attacker.entity, change: *ec * compute_energy_reward_mod(attacker.inventory) * strength_modifier, }); } }, CombatEffect::Buff(b) => { if thread_rng().gen::() < b.chance { emit(ServerEvent::Buff { entity: target.entity, buff_change: BuffChange::Add(b.to_buff( attacker.map(|a| a.uid), applied_damage, strength_modifier, )), }); } }, CombatEffect::Lifesteal(l) => { // Not modified by strength_modifer as damage already is if let Some(attacker_entity) = attacker.map(|a| a.entity) { let change = HealthChange { amount: applied_damage * l, by: attacker.map(|a| a.into()), cause: None, time, }; if change.amount.abs() > Health::HEALTH_EPSILON { emit(ServerEvent::HealthChange { entity: attacker_entity, change, }); } } }, CombatEffect::Poise(p) => { let change = -Poise::apply_poise_reduction(*p, target.inventory) * strength_modifier; if change.abs() > Poise::POISE_EPSILON { let poise_change = PoiseChange { amount: change, impulse: *dir, by: attacker.map(|x| x.into()), cause: Some(damage.damage.source), time, }; emit(ServerEvent::PoiseChange { entity: target.entity, change: poise_change, }); } }, CombatEffect::Heal(h) => { let change = HealthChange { amount: *h * strength_modifier, by: attacker.map(|a| a.into()), cause: None, time, }; if change.amount.abs() > Health::HEALTH_EPSILON { emit(ServerEvent::HealthChange { entity: target.entity, change, }); } }, CombatEffect::Combo(c) => { // Not affected by strength modifier as integer if let Some(attacker_entity) = attacker.map(|a| a.entity) { emit(ServerEvent::ComboChange { entity: attacker_entity, change: *c, }); } }, } } } } for effect in self .effects .iter() .filter(|e| e.target.map_or(true, |t| t == target_group)) .filter(|e| !avoid_effect(e)) { if effect.requirements.iter().all(|req| match req { CombatRequirement::AnyDamage => accumulated_damage > 0.0 && target.health.is_some(), CombatRequirement::Energy(r) => { if let Some(AttackerInfo { entity, energy: Some(e), .. }) = attacker { let sufficient_energy = e.current() >= *r; if sufficient_energy { emit(ServerEvent::EnergyChange { entity, change: -*r, }); } sufficient_energy } else { false } }, CombatRequirement::Combo(r) => { if let Some(AttackerInfo { entity, combo: Some(c), .. }) = attacker { let sufficient_combo = c.counter() >= *r; if sufficient_combo { emit(ServerEvent::ComboChange { entity, change: -(*r as i32), }); } sufficient_combo } else { false } }, }) { is_applied = true; match effect.effect { CombatEffect::Knockback(kb) => { let impulse = kb.calculate_impulse(dir) * strength_modifier; if !impulse.is_approx_zero() { emit(ServerEvent::Knockback { entity: target.entity, impulse, }); } }, CombatEffect::EnergyReward(ec) => { if let Some(attacker) = attacker { emit(ServerEvent::EnergyChange { entity: attacker.entity, change: ec * compute_energy_reward_mod(attacker.inventory) * strength_modifier, }); } }, CombatEffect::Buff(b) => { if thread_rng().gen::() < b.chance { emit(ServerEvent::Buff { entity: target.entity, buff_change: BuffChange::Add(b.to_buff( attacker.map(|a| a.uid), accumulated_damage, strength_modifier, )), }); } }, CombatEffect::Lifesteal(l) => { // Not modified by strength_modifer as damage already is if let Some(attacker_entity) = attacker.map(|a| a.entity) { let change = HealthChange { amount: accumulated_damage * l, by: attacker.map(|a| a.into()), cause: None, time, }; if change.amount.abs() > Health::HEALTH_EPSILON { emit(ServerEvent::HealthChange { entity: attacker_entity, change, }); } } }, CombatEffect::Poise(p) => { let change = -Poise::apply_poise_reduction(p, target.inventory) * strength_modifier; if change.abs() > Poise::POISE_EPSILON { let poise_change = PoiseChange { amount: change, impulse: *dir, by: attacker.map(|x| x.into()), cause: Some(attack_source.into()), time, }; emit(ServerEvent::PoiseChange { entity: target.entity, change: poise_change, }); } }, CombatEffect::Heal(h) => { let change = HealthChange { amount: h * strength_modifier, by: attacker.map(|a| a.into()), cause: None, time, }; if change.amount.abs() > Health::HEALTH_EPSILON { emit(ServerEvent::HealthChange { entity: target.entity, change, }); } }, CombatEffect::Combo(c) => { // Not affected by strength modifier as integer if let Some(attacker_entity) = attacker.map(|a| a.entity) { emit(ServerEvent::ComboChange { entity: attacker_entity, change: c, }); } }, } } } // Emits event to handle things that should happen for any successful attack, // regardless of if the attack had any damages or effects in it if is_applied { emit(ServerEvent::EntityAttackedHook { entity: target.entity, }); } is_applied } } /// Function that checks for unintentional PvP between players. /// /// Returns `false` if attack will create unintentional conflict, /// e.g. if player with PvE mode will harm pets of other players /// or other players will do the same to such player. /// /// If both players have PvP mode enabled, interact with NPC and /// in any other case, this function will return `true` // TODO: add parameter for doing self-harm? pub fn may_harm( alignments: &ReadStorage, players: &ReadStorage, uid_allocator: &UidAllocator, attacker: Option, target: EcsEntity, ) -> bool { // Return owner entity if pet, // or just return entity back otherwise let owner_if_pet = |entity| { let alignment = alignments.get(entity).copied(); if let Some(Alignment::Owned(uid)) = alignment { // return original entity // if can't get owner uid_allocator .retrieve_entity_internal(uid.into()) .unwrap_or(entity) } else { entity } }; // Just return ok if attacker is unknown, it's probably // environment or command. let attacker = match attacker { Some(attacker) => attacker, None => return true, }; // "Dereference" to owner if this is a pet. let attacker = owner_if_pet(attacker); let target = owner_if_pet(target); // Get player components let attacker_info = players.get(attacker); let target_info = players.get(target); // Return `true` if not players. attacker_info .zip(target_info) .map_or(true, |(a, t)| a.may_harm(t)) } #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AttackDamage { damage: Damage, target: Option, effects: Vec, } #[cfg(not(target_arch = "wasm32"))] impl AttackDamage { pub fn new(damage: Damage, target: Option) -> Self { Self { damage, target, effects: Vec::new(), } } #[must_use] pub fn with_effect(mut self, effect: CombatEffect) -> Self { self.effects.push(effect); self } } #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AttackEffect { target: Option, effect: CombatEffect, requirements: Vec, } #[cfg(not(target_arch = "wasm32"))] impl AttackEffect { pub fn new(target: Option, effect: CombatEffect) -> Self { Self { target, effect, requirements: Vec::new(), } } #[must_use] pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self { self.requirements.push(requirement); self } pub fn effect(&self) -> &CombatEffect { &self.effect } } #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub enum CombatEffect { Heal(f32), Buff(CombatBuff), Knockback(Knockback), EnergyReward(f32), Lifesteal(f32), Poise(f32), Combo(i32), } #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CombatRequirement { AnyDamage, Energy(f32), Combo(u32), } #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)] pub enum DamageContributor { Solo(Uid), Group { entity_uid: Uid, group: Group }, } impl DamageContributor { pub fn new(uid: Uid, group: Option) -> Self { if let Some(group) = group { DamageContributor::Group { entity_uid: uid, group, } } else { DamageContributor::Solo(uid) } } pub fn uid(&self) -> Uid { match self { DamageContributor::Solo(uid) => *uid, DamageContributor::Group { entity_uid, group: _, } => *entity_uid, } } } impl From> for DamageContributor { fn from(attacker_info: AttackerInfo) -> Self { DamageContributor::new(attacker_info.uid, attacker_info.group.copied()) } } #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum DamageSource { Buff(BuffKind), Melee, Projectile, Explosion, Falling, Shockwave, Energy, Other, } impl From for DamageSource { fn from(attack: AttackSource) -> Self { match attack { AttackSource::Melee => DamageSource::Melee, AttackSource::Projectile => DamageSource::Projectile, AttackSource::Explosion => DamageSource::Explosion, AttackSource::Shockwave => DamageSource::Shockwave, AttackSource::Beam => DamageSource::Energy, } } } /// DamageKind for the purpose of differentiating damage reduction #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] pub enum DamageKind { /// Bypasses some protection from armor Piercing, /// Reduces energy of target, dealing additional damage when target energy /// is 0 Slashing, /// Deals additional poise damage the more armored the target is Crushing, /// Catch all for remaining damage kinds (TODO: differentiate further with /// staff/sceptre reworks Energy, } const PIERCING_PENETRATION_FRACTION: f32 = 1.0; const SLASHING_ENERGY_FRACTION: f32 = 1.0; const CRUSHING_POISE_FRACTION: f32 = 1.0; #[cfg(not(target_arch = "wasm32"))] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Damage { pub source: DamageSource, pub kind: DamageKind, pub value: f32, } #[cfg(not(target_arch = "wasm32"))] impl Damage { /// Returns the total damage reduction provided by all equipped items pub fn compute_damage_reduction( damage: Option, inventory: Option<&Inventory>, stats: Option<&Stats>, ) -> f32 { let protection = compute_protection(inventory); let penetration = if let Some(damage) = damage { if let DamageKind::Piercing = damage.kind { (damage.value * PIERCING_PENETRATION_FRACTION) .min(protection.unwrap_or(0.0)) .max(0.0) } else { 0.0 } } else { 0.0 }; let protection = protection.map(|p| p - penetration); const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0; let inventory_dr = match protection { Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()), None => 1.0, }; let stats_dr = if let Some(stats) = stats { stats.damage_reduction } else { 0.0 }; 1.0 - (1.0 - inventory_dr) * (1.0 - stats_dr) } pub fn calculate_health_change( self, damage_reduction: f32, damage_contributor: Option, is_crit: bool, crit_mult: f32, damage_modifier: f32, time: Time, ) -> HealthChange { let mut damage = self.value * damage_modifier; let critdamage = if is_crit { damage * (crit_mult - 1.0) } else { 0.0 }; match self.source { DamageSource::Melee | DamageSource::Projectile | DamageSource::Explosion | DamageSource::Shockwave | DamageSource::Energy => { // Critical hit damage += critdamage; // Armor damage *= 1.0 - damage_reduction; HealthChange { amount: -damage, by: damage_contributor, cause: Some(self.source), time, } }, DamageSource::Falling => { // Armor if (damage_reduction - 1.0).abs() < f32::EPSILON { damage = 0.0; } HealthChange { amount: -damage, by: None, cause: Some(self.source), time, } }, DamageSource::Buff(_) | DamageSource::Other => HealthChange { amount: -damage, by: None, cause: Some(self.source), time, }, } } pub fn interpolate_damage(&mut self, frac: f32, min: f32) { let new_damage = min + frac * (self.value - min); self.value = new_damage; } } #[cfg(not(target_arch = "wasm32"))] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Knockback { pub direction: KnockbackDir, pub strength: f32, } #[cfg(not(target_arch = "wasm32"))] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum KnockbackDir { Away, Towards, Up, TowardsUp, } #[cfg(not(target_arch = "wasm32"))] impl Knockback { pub fn calculate_impulse(self, dir: Dir) -> Vec3 { // TEMP until source knockback values have been updated 50.0 * match self.direction { KnockbackDir::Away => self.strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5), KnockbackDir::Towards => { self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5) }, KnockbackDir::Up => self.strength * Vec3::unit_z(), KnockbackDir::TowardsUp => { self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85) }, } } #[must_use] pub fn modify_strength(mut self, power: f32) -> Self { self.strength *= power; self } } #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub struct CombatBuff { pub kind: BuffKind, pub dur_secs: f32, pub strength: CombatBuffStrength, pub chance: f32, } #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] pub enum CombatBuffStrength { DamageFraction(f32), Value(f32), } #[cfg(not(target_arch = "wasm32"))] impl CombatBuffStrength { fn to_strength(self, damage: f32, strength_modifier: f32) -> f32 { match self { // Not affected by strength modifier as damage already is CombatBuffStrength::DamageFraction(f) => damage * f, CombatBuffStrength::Value(v) => v * strength_modifier, } } } impl MulAssign for CombatBuffStrength { fn mul_assign(&mut self, mul: f32) { match self { Self::DamageFraction(ref mut val) | Self::Value(ref mut val) => { *val *= mul; }, } } } #[cfg(not(target_arch = "wasm32"))] impl CombatBuff { fn to_buff(self, uid: Option, damage: f32, strength_modifier: f32) -> Buff { // TODO: Generate BufCategoryId vec (probably requires damage overhaul?) let source = if let Some(uid) = uid { BuffSource::Character { by: uid } } else { BuffSource::Unknown }; Buff::new( self.kind, BuffData::new( self.strength.to_strength(damage, strength_modifier), Some(Duration::from_secs_f32(self.dur_secs)), ), Vec::new(), source, ) } } #[cfg(not(target_arch = "wasm32"))] fn equipped_item_and_tool(inv: &Inventory, slot: EquipSlot) -> Option<(&Item, &Tool)> { inv.equipped(slot).and_then(|i| { if let ItemKind::Tool(tool) = &i.kind() { Some((i, tool)) } else { None } }) } #[cfg(not(target_arch = "wasm32"))] pub fn get_weapons(inv: &Inventory) -> (Option, Option) { ( equipped_item_and_tool(inv, EquipSlot::ActiveMainhand).map(|(_, tool)| tool.kind), equipped_item_and_tool(inv, EquipSlot::ActiveOffhand).map(|(_, tool)| tool.kind), ) } #[cfg(not(target_arch = "wasm32"))] pub fn weapon_rating(item: &T, msm: &MaterialStatManifest) -> f32 { const DAMAGE_WEIGHT: f32 = 2.0; const SPEED_WEIGHT: f32 = 3.0; const CRIT_CHANCE_WEIGHT: f32 = 1.25; const RANGE_WEIGHT: f32 = 0.0; const EFFECT_WEIGHT: f32 = 1.0; const EQUIP_TIME_WEIGHT: f32 = 0.25; const ENERGY_EFFICIENCY_WEIGHT: f32 = 0.0; const BUFF_STRENGTH_WEIGHT: f32 = 0.0; if let ItemKind::Tool(tool) = item.kind() { let stats = tool::Stats::from((msm, item.components(), tool)); // TODO: Look into changing the 0.5 to reflect armor later maybe? // Since it is only for weapon though, it probably makes sense to leave // independent for now let damage_rating = stats.power - 1.0; let speed_rating = stats.speed - 1.0; let crit_chance_rating = stats.crit_chance - 0.1; let range_rating = stats.range; let effect_rating = stats.effect_power - 1.0; let equip_time_rating = 0.5 - stats.equip_time_secs; let energy_efficiency_rating = stats.energy_efficiency; let buff_strength_rating = stats.buff_strength; 0.5 + (1.0 + (damage_rating * DAMAGE_WEIGHT) + (speed_rating * SPEED_WEIGHT) + (crit_chance_rating * CRIT_CHANCE_WEIGHT) + (range_rating * RANGE_WEIGHT) + (effect_rating * EFFECT_WEIGHT) + (equip_time_rating * EQUIP_TIME_WEIGHT) + (energy_efficiency_rating * ENERGY_EFFICIENCY_WEIGHT) + (buff_strength_rating * BUFF_STRENGTH_WEIGHT)) / (DAMAGE_WEIGHT + SPEED_WEIGHT + CRIT_CHANCE_WEIGHT + RANGE_WEIGHT + EFFECT_WEIGHT + EQUIP_TIME_WEIGHT + ENERGY_EFFICIENCY_WEIGHT + BUFF_STRENGTH_WEIGHT) } else { 0.0 } } #[cfg(not(target_arch = "wasm32"))] fn weapon_skills(inventory: &Inventory, skill_set: &SkillSet) -> f32 { let (mainhand, offhand) = get_weapons(inventory); let mainhand_skills = if let Some(tool) = mainhand { skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32 } else { 0.0 }; let offhand_skills = if let Some(tool) = offhand { skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32 } else { 0.0 }; mainhand_skills.max(offhand_skills) } #[cfg(not(target_arch = "wasm32"))] fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 { let mainhand_rating = if let Some((item, _)) = equipped_item_and_tool(inventory, EquipSlot::ActiveMainhand) { weapon_rating(item, msm) } else { 0.0 }; let offhand_rating = if let Some((item, _)) = equipped_item_and_tool(inventory, EquipSlot::ActiveOffhand) { weapon_rating(item, msm) } else { 0.0 }; mainhand_rating.max(offhand_rating) } #[cfg(not(target_arch = "wasm32"))] pub fn combat_rating( inventory: &Inventory, health: &Health, energy: &Energy, poise: &Poise, skill_set: &SkillSet, body: Body, msm: &MaterialStatManifest, ) -> f32 { const WEAPON_WEIGHT: f32 = 1.0; const HEALTH_WEIGHT: f32 = 1.5; const ENERGY_WEIGHT: f32 = 0.5; const SKILLS_WEIGHT: f32 = 1.0; const POISE_WEIGHT: f32 = 0.5; const CRIT_WEIGHT: f32 = 0.5; // Normalized with a standard max health of 100 let health_rating = health.base_max() / 100.0 / (1.0 - Damage::compute_damage_reduction(None, Some(inventory), None)).max(0.00001); // Normalized with a standard max energy of 100 and energy reward multiplier of // x1 let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory))) / 100.0 * compute_energy_reward_mod(Some(inventory)); // Normalized with a standard max poise of 100 let poise_rating = poise.base_max() as f32 / 100.0 / (1.0 - Poise::compute_poise_damage_reduction(inventory)).max(0.00001); // Normalized with a standard crit multiplier of 1.2 let crit_rating = compute_crit_mult(Some(inventory)) / 1.2; // Assumes a standard person has earned 20 skill points in the general skill // tree and 10 skill points for the weapon skill tree let skills_rating = (skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0 + weapon_skills(inventory, skill_set) / 10.0) / 2.0; let weapon_rating = get_weapon_rating(inventory, msm); let combined_rating = (health_rating * HEALTH_WEIGHT + energy_rating * ENERGY_WEIGHT + poise_rating * POISE_WEIGHT + crit_rating * CRIT_WEIGHT + skills_rating * SKILLS_WEIGHT + weapon_rating * WEAPON_WEIGHT) / (HEALTH_WEIGHT + ENERGY_WEIGHT + POISE_WEIGHT + CRIT_WEIGHT + SKILLS_WEIGHT + WEAPON_WEIGHT); // Body multiplier meant to account for an enemy being harder than equipment and // skills would account for. It should only not be 1.0 for non-humanoids combined_rating * body.combat_multiplier() } #[cfg(not(target_arch = "wasm32"))] pub fn compute_crit_mult(inventory: Option<&Inventory>) -> f32 { // Starts with a value of 1.25 when summing the stats from each armor piece, and // defaults to a value of 1.25 if no inventory is equipped inventory.map_or(1.25, |inv| { inv.equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &item.kind() { armor.crit_power() } else { None } }) .fold(1.25, |a, b| a + b) }) } /// Computes the energy reward modifer from worn armor #[cfg(not(target_arch = "wasm32"))] pub fn compute_energy_reward_mod(inventory: Option<&Inventory>) -> f32 { // Starts with a value of 1.0 when summing the stats from each armor piece, and // defaults to a value of 1.0 if no inventory is present inventory.map_or(1.0, |inv| { inv.equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &item.kind() { armor.energy_reward() } else { None } }) .fold(1.0, |a, b| a + b) }) } /// Computes the additive modifier that should be applied to max energy from the /// currently equipped items #[cfg(not(target_arch = "wasm32"))] pub fn compute_max_energy_mod(inventory: Option<&Inventory>) -> f32 { // Defaults to a value of 0 if no inventory is present inventory.map_or(0.0, |inv| { inv.equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &item.kind() { armor.energy_max() } else { None } }) .sum() }) } /// Computes the sneak coefficient from armor. Agent perception distances are /// divided by the resulting f32. #[cfg(not(target_arch = "wasm32"))] pub fn compute_stealth_coefficient(inventory: Option<&Inventory>) -> f32 { // Starts with a value of 2.0 when summing the stats from each armor piece, and // defaults to a value of 2.0 if no inventory is equipped inventory.map_or(2.0, |inv| { inv.equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &item.kind() { armor.stealth() } else { None } }) .fold(2.0, |a, b| a + b.max(0.0)) }) } /// Computes the total protection provided from armor. Is used to determine the /// damage reduction applied to damage received by an entity None indicates that /// the armor equipped makes the entity invulnerable #[cfg(not(target_arch = "wasm32"))] pub fn compute_protection(inventory: Option<&Inventory>) -> Option { inventory.map_or(Some(0.0), |inv| { inv.equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &item.kind() { armor.protection() } else { None } }) .map(|protection| match protection { Protection::Normal(protection) => Some(protection), Protection::Invincible => None, }) .sum::>() }) }