diff --git a/assets/voxygen/i18n/en/common.ron b/assets/voxygen/i18n/en/common.ron index 17b2527f2d..12317c2323 100644 --- a/assets/voxygen/i18n/en/common.ron +++ b/assets/voxygen/i18n/en/common.ron @@ -89,7 +89,7 @@ Is the client up to date?"#, "common.rand_appearance": "Random appearance", "common.rand_name": "Random name", - "common.stats.dps": "DPS", + "common.stats.combat_rating": "CR", "common.stats.power": "Power", "common.stats.speed": "Speed", "common.stats.poise": "Poise", diff --git a/assets/voxygen/i18n/en/hud/bag.ron b/assets/voxygen/i18n/en/hud/bag.ron index 0bad38eee7..0cfca4aaa4 100644 --- a/assets/voxygen/i18n/en/hud/bag.ron +++ b/assets/voxygen/i18n/en/hud/bag.ron @@ -30,7 +30,7 @@ "hud.bag.combat_rating": "Combat Rating", "hud.bag.protection": "Protection", "hud.bag.combat_rating_desc": "Calculated from your\nequipment and health.", - "hud.bag.protection": "Damage reduction through armor", + "hud.bag.protection_desc": "Damage reduction through armor", }, diff --git a/assets/voxygen/i18n/fr_FR/common.ron b/assets/voxygen/i18n/fr_FR/common.ron index e2f1d5fd9a..567bc21e49 100644 --- a/assets/voxygen/i18n/fr_FR/common.ron +++ b/assets/voxygen/i18n/fr_FR/common.ron @@ -87,7 +87,7 @@ Le client est-il à jour?"#, "common.rand_appearance": "Apparence et nom aléatoire", - "common.stats.dps": "DPS", + "common.stats.combat_rating": "CR", "common.stats.power": "Puissance", "common.stats.speed": "Vitesse", "common.stats.poise": "Impact", diff --git a/assets/voxygen/voxel/weapon/staff/Bone_staff.vox b/assets/voxygen/voxel/weapon/staff/bone_staff.vox similarity index 100% rename from assets/voxygen/voxel/weapon/staff/Bone_staff.vox rename to assets/voxygen/voxel/weapon/staff/bone_staff.vox diff --git a/common/src/combat.rs b/common/src/combat.rs index 6a4fcf21fa..f001bbd6b6 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -6,13 +6,13 @@ use crate::{ inventory::{ item::{ armor::Protection, - tool::{Tool, ToolKind}, - Item, ItemKind, MaterialStatManifest, + tool::{self, Tool, ToolKind}, + Item, ItemDesc, ItemKind, MaterialStatManifest, }, slot::EquipSlot, }, poise::PoiseChange, - skills::{SkillGroupKind, SkillSet}, + skills::SkillGroupKind, Body, Combo, Energy, EnergyChange, EnergySource, Health, HealthChange, HealthSource, Inventory, Stats, }, @@ -721,21 +721,55 @@ pub fn get_weapons(inv: &Inventory) -> (Option, Option) { ) } -#[cfg(not(target_arch = "wasm32"))] -fn offensive_rating(inv: &Inventory, skillset: &SkillSet, msm: &MaterialStatManifest) -> f32 { - let active_damage = - equipped_item_and_tool(inv, EquipSlot::Mainhand).map_or(0.0, |(item, tool)| { - tool.base_power(msm, item.components()) - * tool.base_speed(msm, item.components()) - * (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32) - }); - let second_damage = - equipped_item_and_tool(inv, EquipSlot::Offhand).map_or(0.0, |(item, tool)| { - tool.base_power(msm, item.components()) - * tool.base_speed(msm, item.components()) - * (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32) - }); - active_damage.max(second_damage) +pub fn weapon_rating(item: &T, msm: &MaterialStatManifest) -> f32 { + const DAMAGE_WEIGHT: f32 = 2.0; + const POISE_WEIGHT: f32 = 1.0; + + if let ItemKind::Tool(tool) = item.kind() { + let stats = tool::Stats::from((msm, item.components(), tool)); + + let damage_rating = + stats.power * stats.speed * (1.0 + stats.crit_chance * (stats.crit_mult - 1.0)); + let poise_rating = stats.poise_strength * stats.speed; + + (damage_rating * DAMAGE_WEIGHT + poise_rating * POISE_WEIGHT) + / (DAMAGE_WEIGHT + POISE_WEIGHT) + } else { + 0.0 + } +} + +fn weapon_skills(inventory: &Inventory, stats: &Stats) -> f32 { + let (mainhand, offhand) = get_weapons(inventory); + let mainhand_skills = if let Some(tool) = mainhand { + stats.skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32 + } else { + 0.0 + }; + let offhand_skills = if let Some(tool) = offhand { + stats.skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32 + } else { + 0.0 + }; + mainhand_skills.max(offhand_skills) +} + +fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 { + let mainhand_rating = + if let Some((item, _)) = equipped_item_and_tool(inventory, EquipSlot::Mainhand) { + weapon_rating(item, msm) + } else { + 0.0 + }; + + let offhand_rating = + if let Some((item, _)) = equipped_item_and_tool(inventory, EquipSlot::Offhand) { + weapon_rating(item, msm) + } else { + 0.0 + }; + + mainhand_rating.max(offhand_rating) } #[cfg(not(target_arch = "wasm32"))] @@ -746,15 +780,28 @@ pub fn combat_rating( body: Body, msm: &MaterialStatManifest, ) -> f32 { - let defensive_weighting = 1.0; - let offensive_weighting = 1.0; - let defensive_rating = health.maximum() as f32 - / (1.0 - Damage::compute_damage_reduction(Some(inventory), Some(stats))).max(0.00001) - / 100.0; - let offensive_rating = offensive_rating(inventory, &stats.skill_set, msm).max(0.1) - + 0.05 * stats.skill_set.earned_sp(SkillGroupKind::General) as f32; - let combined_rating = (offensive_rating * offensive_weighting - + defensive_rating * defensive_weighting) - / (offensive_weighting + defensive_weighting); + const WEAPON_WEIGHT: f32 = 1.0; + const HEALTH_WEIGHT: f32 = 1.0; + const SKILLS_WEIGHT: f32 = 1.0; + // Assumes a "standard" max health of 100 + let health_rating = health.base_max() as f32 + / 100.0 + / (1.0 - Damage::compute_damage_reduction(Some(inventory), Some(stats))).max(0.00001); + + // 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 = (stats.skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0 + + weapon_skills(inventory, stats) / 10.0) + / 2.0; + + let weapon_rating = get_weapon_rating(inventory, msm); + + let combined_rating = (health_rating * HEALTH_WEIGHT + + skills_rating * SKILLS_WEIGHT + + weapon_rating * WEAPON_WEIGHT) + / (HEALTH_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() } diff --git a/voxygen/src/ui/widgets/item_tooltip.rs b/voxygen/src/ui/widgets/item_tooltip.rs index fdd51c6043..df4af16b09 100644 --- a/voxygen/src/ui/widgets/item_tooltip.rs +++ b/voxygen/src/ui/widgets/item_tooltip.rs @@ -10,6 +10,7 @@ use crate::{ }; use client::Client; use common::{ + combat, comp::item::{armor::Protection, Item, ItemDesc, ItemKind, MaterialStatManifest, Quality}, trade::SitePrices, }; @@ -553,10 +554,10 @@ impl<'a> Widget for ItemTooltip<'a> { let poise_str = tool.base_poise_strength(self.msm, item.components()) * 10.0; let crit_chance = tool.base_crit_chance(self.msm, item.components()) * 100.0; let crit_mult = tool.base_crit_mult(self.msm, item.components()); - let dps = power * speed; + let combat_rating = combat::weapon_rating(&item, self.msm) * 10.0; - // DPS - widget::Text::new(&format!("{:.1}", dps)) + // Combat Rating + widget::Text::new(&format!("{:.1}", combat_rating)) .graphics_for(id) .parent(id) .with_style(self.style.desc) @@ -566,7 +567,7 @@ impl<'a> Widget for ItemTooltip<'a> { .right_from(state.ids.item_frame, H_PAD) .set(state.ids.main_stat, ui); - widget::Text::new(i18n.get("common.stats.dps")) + widget::Text::new(i18n.get("common.stats.combat_rating")) .graphics_for(id) .parent(id) .with_style(self.style.desc) @@ -665,11 +666,12 @@ impl<'a> Widget for ItemTooltip<'a> { ); let crit_mult_diff = util::comparison(tool_stats.crit_mult, equipped_tool_stats.crit_mult); - let equipped_dps = - equipped_tool_stats.power * equipped_tool_stats.speed * 10.0; - let diff_main_stat = util::comparison(dps, equipped_dps); + let equipped_combat_rating = + combat::weapon_rating(&equipped_item, self.msm) * 10.0; + let diff_main_stat = + util::comparison(combat_rating, equipped_combat_rating); - if equipped_dps - dps != 0.0 { + if (equipped_combat_rating - combat_rating).abs() > f32::EPSILON { widget::Text::new(&diff_main_stat.0) .right_from(state.ids.main_stat_text, H_PAD) .graphics_for(id) @@ -679,7 +681,7 @@ impl<'a> Widget for ItemTooltip<'a> { .set(state.ids.diff_main_stat, ui); } - if diff.power != 0.0 { + if diff.power.abs() > f32::EPSILON { widget::Text::new(&format!( "{} {:.1}", &power_diff.0, @@ -693,7 +695,7 @@ impl<'a> Widget for ItemTooltip<'a> { .color(power_diff.1) .set(state.ids.diffs[0], ui); } - if diff.speed != 0.0 { + if diff.speed.abs() > f32::EPSILON { widget::Text::new(&format!("{} {:.1}", &speed_diff.0, &diff.speed)) .align_middle_y_of(state.ids.stats[1]) .right_from(state.ids.stats[1], H_PAD) @@ -703,7 +705,7 @@ impl<'a> Widget for ItemTooltip<'a> { .color(speed_diff.1) .set(state.ids.diffs[1], ui); } - if diff.poise_strength != 0.0 { + if diff.poise_strength.abs() > f32::EPSILON { widget::Text::new(&format!( "{} {:.1}", &poise_strength_diff.0, @@ -717,7 +719,7 @@ impl<'a> Widget for ItemTooltip<'a> { .color(poise_strength_diff.1) .set(state.ids.diffs[2], ui); } - if diff.crit_chance != 0.0 { + if diff.crit_chance.abs() > f32::EPSILON { widget::Text::new(&format!( "{} {:.1}%", &crit_chance_diff.0, @@ -731,7 +733,7 @@ impl<'a> Widget for ItemTooltip<'a> { .color(crit_chance_diff.1) .set(state.ids.diffs[3], ui); } - if diff.crit_mult != 0.0 { + if diff.crit_mult.abs() > f32::EPSILON { widget::Text::new(&format!( "{} {:.1}", &crit_mult_diff.0, &diff.crit_mult