diff --git a/.cargo/config b/.cargo/config index 041039b191..e9a85dd6aa 100644 --- a/.cargo/config +++ b/.cargo/config @@ -21,3 +21,4 @@ tracy-voxygen = "run --bin veloren-voxygen --no-default-features --features trac dbg-voxygen = "run --bin veloren-voxygen --profile debuginfo" # misc swarm = "run --bin swarm --features client/bin_bot,client/tick_network --" +ci-clippy = "clippy --all-targets --locked --features=bin_cmd_doc_gen,bin_compression,bin_csv,bin_graphviz,bin_bot,bin_asset_migrate,asset_tweak" \ No newline at end of file diff --git a/assets/common/material_stats_manifest.ron b/assets/common/material_stats_manifest.ron index 92b2c7b0d3..aaf8057352 100644 --- a/assets/common/material_stats_manifest.ron +++ b/assets/common/material_stats_manifest.ron @@ -1,125 +1,317 @@ // Keep in mind that material stats are multiplied by the form stats, not added (e.g. equip_time_secs is most sensitive to this) -({ - // Metals - "common.items.mineral.ingot.bronze": ( - equip_time_secs: 1.0, - power: 0.75, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.mineral.ingot.iron": ( - equip_time_secs: 1.0, - power: 1.0, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.mineral.ingot.steel": ( - equip_time_secs: 1.0, - power: 1.25, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.mineral.ingot.cobalt": ( - equip_time_secs: 1.0, - power: 1.5, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.mineral.ingot.bloodsteel": ( - equip_time_secs: 1.0, - power: 1.75, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.mineral.ingot.orichalcum": ( - equip_time_secs: 1.0, - power: 2.0, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - // Woods - "common.items.log.wood": ( - equip_time_secs: 1.0, - power: 0.75, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.log.bamboo": ( - equip_time_secs: 1.0, - power: 1.0, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.log.hardwood": ( - equip_time_secs: 1.0, - power: 1.25, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.log.ironwood": ( - equip_time_secs: 1.0, - power: 1.5, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.log.frostwood": ( - equip_time_secs: 1.0, - power: 1.75, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), - "common.items.log.eldwood": ( - equip_time_secs: 1.0, - power: 2.0, - effect_power: 1.0, - speed: 1.0, - crit_chance: 1.0, - range: 1.0, - energy_efficiency: 1.0, - buff_strength: 1.0, - ), -}) +( + tool_stats: { + // Metals + "common.items.mineral.ingot.bronze": ( + equip_time_secs: 1.0, + power: 0.75, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.mineral.ingot.iron": ( + equip_time_secs: 1.0, + power: 1.0, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.mineral.ingot.steel": ( + equip_time_secs: 1.0, + power: 1.25, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.mineral.ingot.cobalt": ( + equip_time_secs: 1.0, + power: 1.5, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.mineral.ingot.bloodsteel": ( + equip_time_secs: 1.0, + power: 1.75, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.mineral.ingot.orichalcum": ( + equip_time_secs: 1.0, + power: 2.0, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + // Woods + "common.items.log.wood": ( + equip_time_secs: 1.0, + power: 0.75, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.log.bamboo": ( + equip_time_secs: 1.0, + power: 1.0, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.log.hardwood": ( + equip_time_secs: 1.0, + power: 1.25, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.log.ironwood": ( + equip_time_secs: 1.0, + power: 1.5, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.log.frostwood": ( + equip_time_secs: 1.0, + power: 1.75, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + "common.items.log.eldwood": ( + equip_time_secs: 1.0, + power: 2.0, + effect_power: 1.0, + speed: 1.0, + crit_chance: 1.0, + range: 1.0, + energy_efficiency: 1.0, + buff_strength: 1.0, + ), + }, + armor_stats: { + // Metals + "common.items.mineral.ingot.bronze": ( + protection: Some(Normal(40.0)), + poise_resilience: Some(Normal(10.0)), + ), + "common.items.mineral.ingot.iron": ( + protection: Some(Normal(80.0)), + poise_resilience: Some(Normal(20.0)), + ), + "common.items.mineral.ingot.steel": ( + protection: Some(Normal(120.0)), + poise_resilience: Some(Normal(30.0)), + ), + "common.items.mineral.ingot.cobalt": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(40.0)), + ), + "common.items.mineral.ingot.bloodsteel": ( + protection: Some(Normal(200.0)), + poise_resilience: Some(Normal(50.0)), + ), + "common.items.mineral.ingot.orichalcum": ( + protection: Some(Normal(240.0)), + poise_resilience: Some(Normal(60.0)), + ), + // Hides + "common.items.crafting_ing.leather.simple_leather": ( + protection: Some(Normal(20.0)), + crit_power: Some(0.333), + stealth: Some(0.333), + ), + "common.items.crafting_ing.leather.thick_leather": ( + protection: Some(Normal(40.0)), + crit_power: Some(0.667), + stealth: Some(0.667), + ), + "common.items.crafting_ing.hide.scales": ( + protection: Some(Normal(60.0)), + crit_power: Some(1.0), + stealth: Some(1.0), + ), + "common.items.crafting_ing.hide.carapace": ( + protection: Some(Normal(80.0)), + crit_power: Some(1.33), + stealth: Some(1.33), + ), + "common.items.crafting_ing.hide.plate": ( + protection: Some(Normal(100.0)), + crit_power: Some(1.67), + stealth: Some(1.67), + ), + "common.items.crafting_ing.hide.dragon_scale": ( + protection: Some(Normal(120.0)), + crit_power: Some(2.0), + stealth: Some(2.0), + ), + // Cloths + "common.items.crafting_ing.cloth.linen": ( + protection: Some(Normal(15.0)), + energy_max: Some(16.7), + energy_reward: Some(0.167), + stealth: Some(0.167), + ), + "common.items.crafting_ing.cloth.wool": ( + protection: Some(Normal(30.0)), + energy_max: Some(33.3), + energy_reward: Some(0.333), + stealth: Some(0.333), + ), + "common.items.crafting_ing.cloth.silk": ( + protection: Some(Normal(45.0)), + energy_max: Some(50.0), + energy_reward: Some(0.5), + stealth: Some(0.5), + ), + "common.items.crafting_ing.cloth.lifecloth": ( + protection: Some(Normal(60.0)), + energy_max: Some(66.7), + energy_reward: Some(0.667), + stealth: Some(0.667), + ), + "common.items.crafting_ing.cloth.moonweave": ( + protection: Some(Normal(75.0)), + energy_max: Some(83.3), + energy_reward: Some(0.833), + stealth: Some(0.833), + ), + "common.items.crafting_ing.cloth.sunsilk": ( + protection: Some(Normal(90.0)), + energy_max: Some(100.0), + energy_reward: Some(1.0), + stealth: Some(1.0), + ), + // Pseudo materials + "common.items.modular.pseudo_material.alchemist": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(20.0)), + energy_max: Some(45.0), + energy_reward: Some(0.5), + crit_power: Some(0.4), + ), + "common.items.modular.pseudo_material.assassin": ( + protection: Some(Normal(50.0)), + poise_resilience: Some(Normal(5.0)), + ), + "common.items.modular.pseudo_material.blacksmith": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(20.0)), + energy_max: Some(45.0), + energy_reward: Some(0.5), + crit_power: Some(0.4), + ), + "common.items.modular.pseudo_material.bonerattler": ( + protection: Some(Normal(100.0)), + ), + "common.items.modular.pseudo_material.chef": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(20.0)), + energy_max: Some(45.0), + energy_reward: Some(0.5), + crit_power: Some(0.4), + ), + "common.items.modular.pseudo_material.cloth_blue": ( + protection: Some(Normal(5.0)), + ), + "common.items.modular.pseudo_material.cloth_green": ( + protection: Some(Normal(5.0)), + ), + "common.items.modular.pseudo_material.cloth_purple": ( + protection: Some(Normal(5.0)), + ), + "common.items.modular.pseudo_material.cultist": ( + protection: Some(Normal(150.0)), + poise_resilience: Some(Normal(20.0)), + energy_max: Some(45.0), + energy_reward: Some(0.5), + crit_power: Some(0.4), + stealth: Some(0.4), + ), + "common.items.modular.pseudo_material.ferocious": ( + protection: Some(Normal(240.0)), + ), + "common.items.modular.pseudo_material.leather_plate": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(40.0)), + ), + "common.items.modular.pseudo_material.merchant": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(20.0)), + energy_max: Some(45.0), + energy_reward: Some(0.5), + crit_power: Some(0.4), + ), + "common.items.modular.pseudo_material.pirate": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(20.0)), + energy_max: Some(45.0), + energy_reward: Some(0.5), + crit_power: Some(0.4), + ), + "common.items.modular.pseudo_material.rugged": ( + protection: Some(Normal(5.0)), + ), + "common.items.modular.pseudo_material.savage": ( + protection: Some(Normal(100.0)), + ), + "common.items.modular.pseudo_material.tarasque": ( + protection: Some(Normal(100.0)), + ), + "common.items.modular.pseudo_material.twigs": ( + protection: Some(Normal(60.0)), + ), + "common.items.modular.pseudo_material.twigsflowers": ( + protection: Some(Normal(60.0)), + ), + "common.items.modular.pseudo_material.twigsleaves": ( + protection: Some(Normal(60.0)), + ), + "common.items.modular.pseudo_material.velorite_mage": ( + protection: Some(Normal(115.0)), + ), + "common.items.modular.pseudo_material.witch": ( + protection: Some(Normal(160.0)), + poise_resilience: Some(Normal(20.0)), + energy_max: Some(45.0), + energy_reward: Some(0.5), + crit_power: Some(0.4), + ), + }, +) diff --git a/common/src/bin/csv_export/main.rs b/common/src/bin/csv_export/main.rs index edd19ad409..ab90b814b3 100644 --- a/common/src/bin/csv_export/main.rs +++ b/common/src/bin/csv_export/main.rs @@ -14,7 +14,7 @@ use veloren_common::{ item::{ armor::{ArmorKind, Protection}, tool::{Hands, Tool, ToolKind}, - Item, + Item, MaterialStatManifest, }, }, generation::{EntityConfig, EntityInfo}, @@ -56,20 +56,22 @@ fn armor_stats() -> Result<(), Box> { continue; } - let protection = match armor.protection() { + let msm = &MaterialStatManifest::load().read(); + + let protection = match armor.stats(msm).protection { Some(Protection::Invincible) => "Invincible".to_string(), Some(Protection::Normal(value)) => value.to_string(), None => "0.0".to_string(), }; - let poise_resilience = match armor.poise_resilience() { + let poise_resilience = match armor.stats(msm).poise_resilience { Some(Protection::Invincible) => "Invincible".to_string(), Some(Protection::Normal(value)) => value.to_string(), None => "0.0".to_string(), }; - let max_energy = armor.energy_max().unwrap_or(0.0).to_string(); - let energy_reward = armor.energy_reward().unwrap_or(0.0).to_string(); - let crit_power = armor.crit_power().unwrap_or(0.0).to_string(); - let stealth = armor.stealth().unwrap_or(0.0).to_string(); + let max_energy = armor.stats(msm).energy_max.unwrap_or(0.0).to_string(); + let energy_reward = armor.stats(msm).energy_reward.unwrap_or(0.0).to_string(); + let crit_power = armor.stats(msm).crit_power.unwrap_or(0.0).to_string(); + let stealth = armor.stats(msm).stealth.unwrap_or(0.0).to_string(); wtr.write_record(&[ item.item_definition_id() diff --git a/common/src/bin/csv_import/main.rs b/common/src/bin/csv_import/main.rs index 65f80d986c..f19ae44b37 100644 --- a/common/src/bin/csv_import/main.rs +++ b/common/src/bin/csv_import/main.rs @@ -12,7 +12,7 @@ use veloren_common::{ comp::{ self, item::{ - armor::{ArmorKind, Protection}, + armor::{ArmorKind, Protection, StatsSource}, tool::{AbilitySpec, Hands, Stats, ToolKind}, ItemDefinitionId, ItemKind, ItemTag, Material, Quality, }, @@ -124,7 +124,7 @@ fn armor_stats() -> Result<(), Box> { None }; - let max_energy = + let energy_max = if let Some(max_energy_raw) = record.get(headers["Max Energy"]) { let value = max_energy_raw.parse().unwrap(); if value == 0.0 { None } else { Some(value) } @@ -174,15 +174,19 @@ fn armor_stats() -> Result<(), Box> { }; let kind = armor.kind; - let armor_stats = comp::item::armor::Stats::new( + let armor_stats = comp::item::armor::Stats { protection, poise_resilience, - max_energy, + energy_max, energy_reward, crit_power, stealth, + ground_contact: Default::default(), + }; + let armor = comp::item::armor::Armor::new( + kind, + StatsSource::Direct(armor_stats), ); - let armor = comp::item::armor::Armor::new(kind, armor_stats); let quality = if let Some(quality_raw) = record.get(headers["Quality"]) { match quality_raw { diff --git a/common/src/combat.rs b/common/src/combat.rs index dfeae2e5cb..6a4ecf9aec 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -133,11 +133,12 @@ impl Attack { source: AttackSource, dir: Dir, damage: Damage, + msm: &MaterialStatManifest, 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); + Damage::compute_damage_reduction(Some(damage), target.inventory, target.stats, msm); let block_reduction = match source { AttackSource::Melee => { if let (Some(CharacterState::BasicBlock(data)), Some(ori)) = @@ -186,6 +187,9 @@ impl Attack { mut emit: impl FnMut(ServerEvent), mut emit_outcome: impl FnMut(Outcome), ) -> bool { + // TODO: Maybe move this higher and pass it as argument into this function? + let msm = &MaterialStatManifest::load().read(); + let AttackOptions { target_dodging, may_harm, @@ -220,6 +224,7 @@ impl Attack { attack_source, dir, damage.damage, + msm, &mut emit, &mut emit_outcome, ); @@ -272,7 +277,7 @@ impl Attack { 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 change = -Poise::apply_poise_reduction(poise, target.inventory, msm); let poise_change = PoiseChange { amount: change, impulse: *dir, @@ -307,7 +312,7 @@ impl Attack { emit(ServerEvent::EnergyChange { entity: attacker.entity, change: *ec - * compute_energy_reward_mod(attacker.inventory) + * compute_energy_reward_mod(attacker.inventory, msm) * strength_modifier, }); } @@ -342,7 +347,7 @@ impl Attack { } }, CombatEffect::Poise(p) => { - let change = -Poise::apply_poise_reduction(*p, target.inventory) + let change = -Poise::apply_poise_reduction(*p, target.inventory, msm) * strength_modifier; if change.abs() > Poise::POISE_EPSILON { let poise_change = PoiseChange { @@ -450,7 +455,7 @@ impl Attack { emit(ServerEvent::EnergyChange { entity: attacker.entity, change: ec - * compute_energy_reward_mod(attacker.inventory) + * compute_energy_reward_mod(attacker.inventory, msm) * strength_modifier, }); } @@ -485,8 +490,8 @@ impl Attack { } }, CombatEffect::Poise(p) => { - let change = - -Poise::apply_poise_reduction(p, target.inventory) * strength_modifier; + let change = -Poise::apply_poise_reduction(p, target.inventory, msm) + * strength_modifier; if change.abs() > Poise::POISE_EPSILON { let poise_change = PoiseChange { amount: change, @@ -755,8 +760,9 @@ impl Damage { damage: Option, inventory: Option<&Inventory>, stats: Option<&Stats>, + msm: &MaterialStatManifest, ) -> f32 { - let protection = compute_protection(inventory); + let protection = compute_protection(inventory, msm); let penetration = if let Some(damage) = damage { if let DamageKind::Piercing = damage.kind { @@ -1058,20 +1064,20 @@ pub fn combat_rating( // 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); + / (1.0 - Damage::compute_damage_reduction(None, Some(inventory), None, msm)).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)); + let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory), msm)) / 100.0 + * compute_energy_reward_mod(Some(inventory), msm); // 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); + / (1.0 - Poise::compute_poise_damage_reduction(inventory, msm)).max(0.00001); // Normalized with a standard crit multiplier of 1.2 - let crit_rating = compute_crit_mult(Some(inventory)) / 1.2; + let crit_rating = compute_crit_mult(Some(inventory), msm) / 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 @@ -1100,14 +1106,14 @@ pub fn combat_rating( } #[cfg(not(target_arch = "wasm32"))] -pub fn compute_crit_mult(inventory: Option<&Inventory>) -> f32 { +pub fn compute_crit_mult(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> 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() + armor.stats(msm).crit_power } else { None } @@ -1118,14 +1124,14 @@ pub fn compute_crit_mult(inventory: Option<&Inventory>) -> f32 { /// Computes the energy reward modifer from worn armor #[cfg(not(target_arch = "wasm32"))] -pub fn compute_energy_reward_mod(inventory: Option<&Inventory>) -> f32 { +pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> 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() + armor.stats(msm).energy_reward } else { None } @@ -1137,13 +1143,13 @@ pub fn compute_energy_reward_mod(inventory: Option<&Inventory>) -> f32 { /// 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 { +pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> 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() + armor.stats(msm).energy_max } else { None } @@ -1158,10 +1164,11 @@ pub fn compute_max_energy_mod(inventory: Option<&Inventory>) -> f32 { pub fn perception_dist_multiplier_from_stealth( inventory: Option<&Inventory>, character_state: Option<&CharacterState>, + msm: &MaterialStatManifest, ) -> f32 { const SNEAK_MULTIPLIER: f32 = 0.7; - let item_stealth_multiplier = stealth_multiplier_from_items(inventory); + let item_stealth_multiplier = stealth_multiplier_from_items(inventory, msm); let is_sneaking = character_state.map_or(false, |state| state.is_stealthy()); let multiplier = item_stealth_multiplier * if is_sneaking { SNEAK_MULTIPLIER } else { 1.0 }; @@ -1170,12 +1177,15 @@ pub fn perception_dist_multiplier_from_stealth( } #[cfg(not(target_arch = "wasm32"))] -pub fn stealth_multiplier_from_items(inventory: Option<&Inventory>) -> f32 { +pub fn stealth_multiplier_from_items( + inventory: Option<&Inventory>, + msm: &MaterialStatManifest, +) -> f32 { let stealth_sum = inventory.map_or(0.0, |inv| { inv.equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &*item.kind() { - armor.stealth() + armor.stats(msm).stealth } else { None } @@ -1190,12 +1200,15 @@ pub fn stealth_multiplier_from_items(inventory: Option<&Inventory>) -> f32 { /// 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 { +pub fn compute_protection( + inventory: Option<&Inventory>, + msm: &MaterialStatManifest, +) -> Option { inventory.map_or(Some(0.0), |inv| { inv.equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &*item.kind() { - armor.protection() + armor.stats(msm).protection } else { None } diff --git a/common/src/comp/inventory/item/armor.rs b/common/src/comp/inventory/item/armor.rs index ebdb2f8b61..26baf0152e 100644 --- a/common/src/comp/inventory/item/armor.rs +++ b/common/src/comp/inventory/item/armor.rs @@ -1,11 +1,15 @@ use crate::{ - comp::item::Rgb, + comp::item::{MaterialStatManifest, Rgb}, terrain::{Block, BlockKind}, }; use serde::{Deserialize, Serialize}; -use std::{cmp::Ordering, ops::Sub}; +use std::{ + cmp::Ordering, + ops::{Mul, Sub}, +}; +use strum::{EnumIter, IntoEnumIterator}; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)] pub enum ArmorKind { Shoulder, Chest, @@ -69,66 +73,66 @@ impl Friction { } } -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)] pub struct Stats { /// Protection is non-linearly transformed (following summation) to a damage /// reduction using (prot / (60 + prot)) - protection: Option, + pub protection: Option, /// Poise protection is non-linearly transformed (following summation) to a /// poise damage reduction using (prot / (60 + prot)) - poise_resilience: Option, + pub poise_resilience: Option, /// Energy max is summed, and then applied directly to the max energy stat - energy_max: Option, + pub energy_max: Option, /// Energy recovery is summed, and then added to 1.0. When attacks reward /// energy, it is then multiplied by this value before the energy is /// rewarded. - energy_reward: Option, + pub energy_reward: Option, /// Crit power is summed, and then added to the default crit multiplier of /// 1.25. Damage is multiplied by this value when an attack crits. - crit_power: Option, + pub crit_power: Option, /// Stealth is summed along with the base stealth bonus (2.0), and then /// the agent's perception distance is divided by this value - stealth: Option, + pub stealth: Option, /// Ground contact type, mostly for shoes #[serde(default)] - ground_contact: Friction, + pub ground_contact: Friction, } impl Stats { - // DO NOT USE UNLESS YOU KNOW WHAT YOU ARE DOING - // Added for csv import of stats - pub fn new( - protection: Option, - poise_resilience: Option, - energy_max: Option, - energy_reward: Option, - crit_power: Option, - stealth: Option, - ) -> Self { - Self { - protection, - poise_resilience, - energy_max, - energy_reward, - crit_power, - stealth, + fn none() -> Self { + Stats { + protection: None, + poise_resilience: None, + energy_max: None, + energy_reward: None, + crit_power: None, + stealth: None, ground_contact: Friction::Normal, } } +} - pub fn protection(&self) -> Option { self.protection } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum StatsSource { + Direct(Stats), + FromSet(String), +} - pub fn poise_resilience(&self) -> Option { self.poise_resilience } +impl Mul for Stats { + type Output = Self; - pub fn energy_max(&self) -> Option { self.energy_max } - - pub fn energy_reward(&self) -> Option { self.energy_reward } - - pub fn crit_power(&self) -> Option { self.crit_power } - - pub fn stealth(&self) -> Option { self.stealth } - - pub fn ground_contact(&self) -> Friction { self.ground_contact } + fn mul(self, val: f32) -> Self::Output { + Stats { + protection: self.protection.map(|a| a * val), + poise_resilience: self.poise_resilience.map(|a| a * val), + energy_max: self.energy_max.map(|a| a * val), + energy_reward: self.energy_reward.map(|a| a * val), + crit_power: self.crit_power.map(|a| a * val), + stealth: self.stealth.map(|a| a * val), + // There is nothing to multiply, it is just an enum + ground_contact: self.ground_contact, + } + } } impl Sub for Stats { @@ -177,6 +181,17 @@ impl Sub for Protection { } } +impl Mul for Protection { + type Output = Self; + + fn mul(self, val: f32) -> Self::Output { + match self { + Protection::Invincible => Protection::Invincible, + Protection::Normal(a) => Protection::Normal(a * val), + } + } +} + impl PartialOrd for Protection { fn partial_cmp(&self, other: &Self) -> Option { match (*self, *other) { @@ -191,25 +206,39 @@ impl PartialOrd for Protection { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Armor { pub kind: ArmorKind, - pub stats: Stats, + stats: StatsSource, } impl Armor { - pub fn new(kind: ArmorKind, stats: Stats) -> Self { Self { kind, stats } } + pub fn new(kind: ArmorKind, stats: StatsSource) -> Self { Self { kind, stats } } - pub fn protection(&self) -> Option { self.stats.protection } + pub fn stats(&self, msm: &MaterialStatManifest) -> Stats { + match &self.stats { + StatsSource::Direct(stats) => *stats, + StatsSource::FromSet(set) => { + let set_stats = msm.armor_stats(set).unwrap_or_else(Stats::none); + let armor_kind_weight = |kind| match kind { + ArmorKind::Shoulder => 2.0, + ArmorKind::Chest => 3.0, + ArmorKind::Belt => 0.5, + ArmorKind::Hand => 1.0, + ArmorKind::Pants => 2.0, + ArmorKind::Foot => 1.0, + ArmorKind::Back => 0.5, + ArmorKind::Ring => 0.0, + ArmorKind::Neck => 0.0, + ArmorKind::Head => 0.0, + ArmorKind::Tabard => 0.0, + ArmorKind::Bag => 0.0, + }; - pub fn poise_resilience(&self) -> Option { self.stats.poise_resilience } + let armor_weights_sum: f32 = ArmorKind::iter().map(armor_kind_weight).sum(); + let multiplier = armor_kind_weight(self.kind) / armor_weights_sum; - pub fn energy_max(&self) -> Option { self.stats.energy_max } - - pub fn energy_reward(&self) -> Option { self.stats.energy_reward } - - pub fn crit_power(&self) -> Option { self.stats.crit_power } - - pub fn stealth(&self) -> Option { self.stats.stealth } - - pub fn ground_contact(&self) -> Friction { self.stats.ground_contact } + set_stats * multiplier + }, + } + } #[cfg(test)] pub fn test_armor( @@ -219,7 +248,7 @@ impl Armor { ) -> Armor { Armor { kind, - stats: Stats { + stats: StatsSource::Direct(Stats { protection: Some(protection), poise_resilience: Some(poise_resilience), energy_max: None, @@ -227,7 +256,7 @@ impl Armor { crit_power: None, stealth: None, ground_contact: Friction::Normal, - }, + }), } } } diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 57643ebec5..450d556ea9 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -4,8 +4,8 @@ pub mod modular; pub mod tool; // Reexports -pub use modular::{ModularBase, ModularComponent}; -pub use tool::{AbilitySet, AbilitySpec, Hands, MaterialStatManifest, Tool, ToolKind}; +pub use modular::{MaterialStatManifest, ModularBase, ModularComponent}; +pub use tool::{AbilitySet, AbilitySpec, Hands, Tool, ToolKind}; use crate::{ assets::{self, AssetExt, BoxedError, Error}, diff --git a/common/src/comp/inventory/item/modular.rs b/common/src/comp/inventory/item/modular.rs index 84be27f07b..a984020c8c 100644 --- a/common/src/comp/inventory/item/modular.rs +++ b/common/src/comp/inventory/item/modular.rs @@ -1,8 +1,12 @@ use super::{ - tool::{self, AbilityMap, AbilitySpec, Hands, MaterialStatManifest}, + armor, + tool::{self, AbilityMap, AbilitySpec, Hands}, Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, Material, Quality, ToolKind, }; -use crate::{assets::AssetExt, recipe}; +use crate::{ + assets::{self, Asset, AssetExt, AssetHandle}, + recipe, +}; use common_base::dev_panic; use hashbrown::HashMap; use lazy_static::lazy_static; @@ -20,6 +24,28 @@ macro_rules! modular_item_id_prefix { }; } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MaterialStatManifest { + tool_stats: HashMap, + armor_stats: HashMap, +} + +impl MaterialStatManifest { + pub fn load() -> AssetHandle { Self::load_expect("common.material_stats_manifest") } + + pub fn armor_stats(&self, key: &str) -> Option { + self.armor_stats.get(key).copied() + } +} + +// This could be a Compound that also loads the keys, but the RecipeBook +// Compound impl already does that, so checking for existence here is redundant. +impl Asset for MaterialStatManifest { + type Loader = assets::RonLoader; + + const EXTENSION: &'static str = "ron"; +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum ModularBase { Tool, @@ -241,7 +267,7 @@ impl ModularComponent { .filter_map(|comp| { comp.item_definition_id() .itemdef_id() - .and_then(|id| msm.0.get(id)) + .and_then(|id| msm.tool_stats.get(id)) .copied() .zip(Some(1)) }) diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index f4891688aa..ed61131771 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -225,21 +225,6 @@ impl DivAssign for Stats { fn div_assign(&mut self, scalar: usize) { *self = *self / (scalar as f32); } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MaterialStatManifest(pub HashMap); - -impl MaterialStatManifest { - pub fn load() -> AssetHandle { Self::load_expect("common.material_stats_manifest") } -} - -// This could be a Compound that also loads the keys, but the RecipeBook -// Compound impl already does that, so checking for existence here is redundant. -impl Asset for MaterialStatManifest { - type Loader = assets::RonLoader; - - const EXTENSION: &'static str = "ron"; -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Tool { pub kind: ToolKind, diff --git a/common/src/comp/inventory/loadout.rs b/common/src/comp/inventory/loadout.rs index 7b3dfb1aab..dd2eee1bc8 100644 --- a/common/src/comp/inventory/loadout.rs +++ b/common/src/comp/inventory/loadout.rs @@ -408,7 +408,7 @@ impl Loadout { pub fn persistence_update_all_item_states( &mut self, ability_map: &item::tool::AbilityMap, - msm: &item::tool::MaterialStatManifest, + msm: &item::MaterialStatManifest, ) { self.slots.iter_mut().for_each(|slot| { if let Some(item) = &mut slot.slot { diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index e5cdaa2988..c62ac0f735 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -808,7 +808,7 @@ impl Inventory { pub fn persistence_update_all_item_states( &mut self, ability_map: &item::tool::AbilityMap, - msm: &item::tool::MaterialStatManifest, + msm: &item::MaterialStatManifest, ) { self.slots_mut().for_each(|slot| { if let Some(item) = slot { diff --git a/common/src/comp/inventory/trade_pricing.rs b/common/src/comp/inventory/trade_pricing.rs index 95d295d70a..e1f73c0ce6 100644 --- a/common/src/comp/inventory/trade_pricing.rs +++ b/common/src/comp/inventory/trade_pricing.rs @@ -686,14 +686,16 @@ impl TradePricing { #[cfg(test)] fn print_sorted(&self) { - use crate::comp::item::{armor, ItemKind}; + use crate::comp::item::{armor, ItemKind, MaterialStatManifest}; println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,"); fn more_information(i: &Item, p: f32) -> (String, &'static str) { + let msm = &MaterialStatManifest::load().read(); + if let ItemKind::Armor(a) = &*i.kind() { ( - match a.protection() { + match a.stats(msm).protection { Some(armor::Protection::Invincible) => "Invincible".into(), Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p), None => "0.0".into(), diff --git a/common/src/comp/poise.rs b/common/src/comp/poise.rs index dcc20e7416..fa36a0e452 100644 --- a/common/src/comp/poise.rs +++ b/common/src/comp/poise.rs @@ -2,7 +2,7 @@ use crate::{ combat::{DamageContributor, DamageSource}, comp::{ self, - inventory::item::{armor::Protection, ItemKind}, + inventory::item::{armor::Protection, ItemKind, MaterialStatManifest}, CharacterState, Inventory, }, resources::Time, @@ -226,12 +226,15 @@ impl Poise { } /// Returns the total poise damage reduction provided by all equipped items - pub fn compute_poise_damage_reduction(inventory: &Inventory) -> f32 { + pub fn compute_poise_damage_reduction( + inventory: &Inventory, + msm: &MaterialStatManifest, + ) -> f32 { let protection = inventory .equipped_items() .filter_map(|item| { if let ItemKind::Armor(armor) = &*item.kind() { - armor.poise_resilience() + armor.stats(msm).poise_resilience } else { None } @@ -249,9 +252,13 @@ impl Poise { /// Modifies a poise change when optionally given an inventory to aid in /// calculation of poise damage reduction - pub fn apply_poise_reduction(value: f32, inventory: Option<&Inventory>) -> f32 { + pub fn apply_poise_reduction( + value: f32, + inventory: Option<&Inventory>, + msm: &MaterialStatManifest, + ) -> f32 { inventory.map_or(value, |inv| { - value * (1.0 - Poise::compute_poise_damage_reduction(inv)) + value * (1.0 - Poise::compute_poise_damage_reduction(inv, msm)) }) } } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 45b3813e5c..883740ee51 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -312,7 +312,7 @@ pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) { footwear = data.inventory.and_then(|inv| { inv.equipped(EquipSlot::Armor(ArmorSlot::Feet)) .map(|armor| match armor.kind().as_ref() { - ItemKind::Armor(a) => a.ground_contact(), + ItemKind::Armor(a) => a.stats(data.msm).ground_contact, _ => crate::comp::inventory::item::armor::Friction::Normal, }) }); @@ -1084,7 +1084,7 @@ pub fn get_crit_data(data: &JoinData<'_>, ai: AbilityInfo) -> (f32, f32) { }) .unwrap_or(DEFAULT_CRIT_CHANCE); - let crit_mult = combat::compute_crit_mult(data.inventory); + let crit_mult = combat::compute_crit_mult(data.inventory, data.msm); (crit_chance, crit_mult) } diff --git a/common/systems/src/buff.rs b/common/systems/src/buff.rs index bd50c13496..e1d94d57c3 100644 --- a/common/systems/src/buff.rs +++ b/common/systems/src/buff.rs @@ -7,6 +7,7 @@ use common::{ Buffs, }, fluid_dynamics::{Fluid, LiquidKind}, + item::MaterialStatManifest, Energy, Group, Health, HealthChange, Inventory, LightEmitter, ModifierKind, PhysicsState, Stats, }, @@ -21,8 +22,8 @@ use common_ecs::{Job, Origin, ParMode, Phase, System}; use hashbrown::HashMap; use rayon::iter::ParallelIterator; use specs::{ - saveload::MarkerAllocator, shred::ResourceId, Entities, Join, ParJoin, Read, ReadStorage, - SystemData, World, WriteStorage, + saveload::MarkerAllocator, shred::ResourceId, Entities, Join, ParJoin, Read, ReadExpect, + ReadStorage, SystemData, World, WriteStorage, }; use std::time::Duration; @@ -38,6 +39,7 @@ pub struct ReadData<'a> { groups: ReadStorage<'a, Group>, uid_allocator: Read<'a, UidAllocator>, time: Read<'a, Time>, + msm: ReadExpect<'a, MaterialStatManifest>, } #[derive(Default)] @@ -207,6 +209,7 @@ impl<'a> System<'a> for Sys { None, read_data.inventories.get(entity), Some(&stat), + &read_data.msm, ); if (damage_reduction - 1.0).abs() < f32::EPSILON { for (id, buff) in buff_comp.buffs.iter() { diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index a6346ea2d1..1fb6761f51 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -2,6 +2,7 @@ use common::{ combat, comp::{ self, + item::MaterialStatManifest, skills::{GeneralSkill, Skill}, Body, CharacterState, Combo, Energy, Health, Inventory, Poise, PoiseChange, Pos, SkillSet, Stats, StatsModifier, @@ -11,7 +12,8 @@ use common::{ }; use common_ecs::{Job, Origin, Phase, System}; use specs::{ - shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData, World, Write, WriteStorage, + shred::ResourceId, Entities, Join, Read, ReadExpect, ReadStorage, SystemData, World, Write, + WriteStorage, }; use vek::Vec3; @@ -28,6 +30,7 @@ pub struct ReadData<'a> { bodies: ReadStorage<'a, Body>, char_states: ReadStorage<'a, CharacterState>, inventories: ReadStorage<'a, Inventory>, + msm: ReadExpect<'a, MaterialStatManifest>, } /// This system kills players, levels them up, and regenerates energy. @@ -100,7 +103,7 @@ impl<'a> System<'a> for Sys { // Calculates energy scaling from stats and inventory let energy_mods = StatsModifier { add_mod: stat.max_energy_modifiers.add_mod - + combat::compute_max_energy_mod(inventory), + + combat::compute_max_energy_mod(inventory, &read_data.msm), mult_mod: stat.max_energy_modifiers.mult_mod, }; diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index d14a109ad4..3f84761385 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -558,7 +558,8 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3) let inventories = ecs.read_storage::(); let stats = ecs.read_storage::(); - let time = server.state.ecs().read_resource::