Put material stats in their own manifest, and multiply a form's stats by the weighted average of the material multipliers.

This commit is contained in:
Avi Weinstock 2021-02-23 15:29:27 -05:00
parent 4e57678f34
commit 78014d7d3b
34 changed files with 366 additions and 149 deletions

View File

@ -5,5 +5,5 @@ ItemDef(
kind: "BloodsteelIngot", kind: "BloodsteelIngot",
), ),
quality: Common, quality: Common,
tags: [MetalIngot(1.75)], tags: [MetalIngot],
) )

View File

@ -5,5 +5,5 @@ ItemDef(
kind: "BronzeIngot", kind: "BronzeIngot",
), ),
quality: Common, quality: Common,
tags: [MetalIngot(0.75)], tags: [MetalIngot],
) )

View File

@ -5,5 +5,5 @@ ItemDef(
kind: "CobaltIngot", kind: "CobaltIngot",
), ),
quality: Common, quality: Common,
tags: [MetalIngot(1.5)], tags: [MetalIngot],
) )

View File

@ -5,5 +5,5 @@ ItemDef(
kind: "CopperIngot", kind: "CopperIngot",
), ),
quality: Common, quality: Common,
tags: [MetalIngot(0.4)], tags: [MetalIngot],
) )

View File

@ -5,5 +5,5 @@ ItemDef(
kind: "IronIngot", kind: "IronIngot",
), ),
quality: Common, quality: Common,
tags: [MetalIngot(1.0)], tags: [MetalIngot],
) )

View File

@ -5,5 +5,5 @@ ItemDef(
kind: "SteelIngot", kind: "SteelIngot",
), ),
quality: Common, quality: Common,
tags: [MetalIngot(1.25)], tags: [MetalIngot],
) )

View File

@ -0,0 +1,45 @@
// Keep in mind that material stats are multiplied by the form stats, not added (e.g. equip_time_millis is most sensitive to this)
({
"common.items.crafting_ing.bloodsteel_ingot": (
equip_time_millis: 1,
power: 1.75,
poise_strength: 1.75,
speed: 1.75,
),
"common.items.crafting_ing.bronze_ingot": (
equip_time_millis: 1,
power: 0.75,
poise_strength: 0.75,
speed: 0.75,
),
"common.items.crafting_ing.cobalt_ingot": (
equip_time_millis: 1,
power: 1.5,
poise_strength: 1.5,
speed: 1.5,
),
"common.items.crafting_ing.copper_ingot": (
equip_time_millis: 1,
power: 0.4,
poise_strength: 0.4,
speed: 0.4,
),
"common.items.crafting_ing.iron_ingot": (
equip_time_millis: 1,
power: 1.0,
poise_strength: 1.0,
speed: 1.0,
),
"common.items.crafting_ing.steel_ingot": (
equip_time_millis: 1,
power: 1.25,
poise_strength: 1.25,
speed: 1.25,
),
"common.items.crafting_ing.tin_ingot": (
equip_time_millis: 1,
power: 0.25,
poise_strength: 0.25,
speed: 0.25,
),
})

View File

@ -361,13 +361,13 @@
[ [
(Tag(ClothItem), 1), (Tag(ClothItem), 1),
(Item("common.items.crafting_tools.sewing_set"), 0), (Item("common.items.crafting_tools.sewing_set"), 0),
] ],
), ),
"metal_blade": ( "metal_blade": (
("common.items.crafting_ing.modular.damage.sword.metal_blade", 1), ("common.items.crafting_ing.modular.damage.sword.metal_blade", 1),
[ [
(Tag(MetalIngot(0.0)), 5), (Tag(MetalIngot), 5),
(Item("common.items.crafting_tools.craftsman_hammer"), 0), (Item("common.items.crafting_tools.craftsman_hammer"), 0),
] ],
), ),
} }

View File

@ -253,6 +253,7 @@ impl Client {
client_timeout, client_timeout,
world_map, world_map,
recipe_book, recipe_book,
material_stats,
ability_map, ability_map,
} => { } => {
// Initialize `State` // Initialize `State`
@ -264,6 +265,7 @@ impl Client {
let entity = state.ecs_mut().apply_entity_package(entity_package); let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day; *state.ecs_mut().write_resource() = time_of_day;
*state.ecs_mut().write_resource() = material_stats;
state.ecs_mut().insert(ability_map); state.ecs_mut().insert(ability_map);
let map_size_lg = common::terrain::MapSizeLg::new(world_map.dimensions_lg) let map_size_lg = common::terrain::MapSizeLg::new(world_map.dimensions_lg)

View File

@ -3,7 +3,7 @@ use crate::sync;
use authc::AuthClientError; use authc::AuthClientError;
use common::{ use common::{
character::{self, CharacterItem}, character::{self, CharacterItem},
comp::{self, invite::InviteKind}, comp::{self, invite::InviteKind, item::MaterialStatManifest},
outcome::Outcome, outcome::Outcome,
recipe::RecipeBook, recipe::RecipeBook,
resources::TimeOfDay, resources::TimeOfDay,
@ -56,6 +56,7 @@ pub enum ServerInit {
client_timeout: Duration, client_timeout: Duration,
world_map: crate::msg::world_msg::WorldMapMsg, world_map: crate::msg::world_msg::WorldMapMsg,
recipe_book: RecipeBook, recipe_book: RecipeBook,
material_stats: MaterialStatManifest,
ability_map: comp::item::tool::AbilityMap, ability_map: comp::item::tool::AbilityMap,
}, },
} }

View File

@ -7,7 +7,7 @@ use crate::{
item::{ item::{
armor::Protection, armor::Protection,
tool::{Tool, ToolKind}, tool::{Tool, ToolKind},
Item, ItemKind, Item, ItemKind, MaterialStatManifest,
}, },
slot::EquipSlot, slot::EquipSlot,
}, },
@ -654,30 +654,36 @@ pub fn get_weapons(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
fn offensive_rating(inv: &Inventory, skillset: &SkillSet) -> f32 { fn offensive_rating(inv: &Inventory, skillset: &SkillSet, msm: &MaterialStatManifest) -> f32 {
let active_damage = let active_damage =
equipped_item_and_tool(inv, EquipSlot::Mainhand).map_or(0.0, |(item, tool)| { equipped_item_and_tool(inv, EquipSlot::Mainhand).map_or(0.0, |(item, tool)| {
tool.base_power(item.components()) tool.base_power(msm, item.components())
* tool.base_speed(item.components()) * tool.base_speed(msm, item.components())
* (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32) * (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32)
}); });
let second_damage = let second_damage =
equipped_item_and_tool(inv, EquipSlot::Offhand).map_or(0.0, |(item, tool)| { equipped_item_and_tool(inv, EquipSlot::Offhand).map_or(0.0, |(item, tool)| {
tool.base_power(item.components()) tool.base_power(msm, item.components())
* tool.base_speed(item.components()) * tool.base_speed(msm, item.components())
* (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32) * (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32)
}); });
active_damage.max(second_damage) active_damage.max(second_damage)
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn combat_rating(inventory: &Inventory, health: &Health, stats: &Stats, body: Body) -> f32 { pub fn combat_rating(
inventory: &Inventory,
health: &Health,
stats: &Stats,
body: Body,
msm: &MaterialStatManifest,
) -> f32 {
let defensive_weighting = 1.0; let defensive_weighting = 1.0;
let offensive_weighting = 1.0; let offensive_weighting = 1.0;
let defensive_rating = health.maximum() as f32 let defensive_rating = health.maximum() as f32
/ (1.0 - Damage::compute_damage_reduction(inventory)).max(0.00001) / (1.0 - Damage::compute_damage_reduction(inventory)).max(0.00001)
/ 100.0; / 100.0;
let offensive_rating = offensive_rating(inventory, &stats.skill_set).max(0.1) let offensive_rating = offensive_rating(inventory, &stats.skill_set, msm).max(0.1)
+ 0.05 * stats.skill_set.earned_sp(SkillGroupKind::General) as f32; + 0.05 * stats.skill_set.earned_sp(SkillGroupKind::General) as f32;
let combined_rating = (offensive_rating * offensive_weighting let combined_rating = (offensive_rating * offensive_weighting
+ defensive_rating * defensive_weighting) + defensive_rating * defensive_weighting)

View File

@ -4,7 +4,7 @@ pub mod tool;
// Reexports // Reexports
pub use modular::{ModularComponent, ModularComponentKind, ModularComponentTag}; pub use modular::{ModularComponent, ModularComponentKind, ModularComponentTag};
pub use tool::{AbilitySet, Hands, Tool, ToolKind, UniqueKind}; pub use tool::{AbilitySet, Hands, MaterialStatManifest, Tool, ToolKind, UniqueKind};
use crate::{ use crate::{
assets::{self, AssetExt, Error}, assets::{self, AssetExt, Error},
@ -87,25 +87,11 @@ pub trait TagExampleInfo {
fn exemplar_identifier(&self) -> &'static str; fn exemplar_identifier(&self) -> &'static str;
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum ItemTag { pub enum ItemTag {
ClothItem, ClothItem,
ModularComponent(ModularComponentTag), ModularComponent(ModularComponentTag),
MetalIngot(f32), MetalIngot,
}
// The PartialEq implementation for ItemTag is used for determining whether a
// RecipeInput matches
impl PartialEq for ItemTag {
fn eq(&self, other: &Self) -> bool {
use ItemTag::*;
match (self, other) {
(ClothItem, ClothItem) => true,
(ModularComponent(a), ModularComponent(b)) => a == b,
(MetalIngot(_), MetalIngot(_)) => true,
_ => false,
}
}
} }
impl TagExampleInfo for ItemTag { impl TagExampleInfo for ItemTag {
@ -113,7 +99,7 @@ impl TagExampleInfo for ItemTag {
match self { match self {
ItemTag::ClothItem => "cloth item", ItemTag::ClothItem => "cloth item",
ItemTag::ModularComponent(kind) => kind.name(), ItemTag::ModularComponent(kind) => kind.name(),
ItemTag::MetalIngot(_) => "metal ingot", ItemTag::MetalIngot => "metal ingot",
} }
} }
@ -121,7 +107,7 @@ impl TagExampleInfo for ItemTag {
match self { match self {
ItemTag::ClothItem => "common.items.tag_examples.cloth_item", ItemTag::ClothItem => "common.items.tag_examples.cloth_item",
ItemTag::ModularComponent(tag) => tag.exemplar_identifier(), ItemTag::ModularComponent(tag) => tag.exemplar_identifier(),
ItemTag::MetalIngot(_) => "common.items.tag_examples.metal_ingot", ItemTag::MetalIngot => "common.items.tag_examples.metal_ingot",
} }
} }
} }
@ -229,10 +215,12 @@ pub struct ItemConfig {
pub dodge_ability: Option<CharacterAbility>, pub dodge_ability: Option<CharacterAbility>,
} }
impl From<(&ItemKind, &[Item], &AbilityMap)> for ItemConfig { impl From<(&ItemKind, &[Item], &AbilityMap, &MaterialStatManifest)> for ItemConfig {
fn from((item_kind, components, map): (&ItemKind, &[Item], &AbilityMap)) -> Self { fn from(
(item_kind, components, map, msm): (&ItemKind, &[Item], &AbilityMap, &MaterialStatManifest),
) -> Self {
if let ItemKind::Tool(tool) = item_kind { if let ItemKind::Tool(tool) = item_kind {
let abilities = tool.get_abilities(components, map); let abilities = tool.get_abilities(msm, components, map);
return ItemConfig { return ItemConfig {
abilities, abilities,
@ -389,12 +377,16 @@ impl Item {
// loadout when no weapon is present // loadout when no weapon is present
pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") } pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") }
pub fn new_from_item_def(inner_item: Arc<ItemDef>, input_components: &[Item]) -> Self { pub fn new_from_item_def(
inner_item: Arc<ItemDef>,
input_components: &[Item],
msm: &MaterialStatManifest,
) -> Self {
let mut components = Vec::new(); let mut components = Vec::new();
if inner_item.is_modular() { if inner_item.is_modular() {
// recipe ensures that types match (i.e. no axe heads on a sword hilt, or double // recipe ensures that types match (i.e. no axe heads on a sword hilt, or double
// sword blades) // sword blades)
components.extend(input_components.iter().map(|comp| comp.duplicate())); components.extend(input_components.iter().map(|comp| comp.duplicate(msm)));
} }
let mut item = Item { let mut item = Item {
@ -405,7 +397,7 @@ impl Item {
item_def: inner_item, item_def: inner_item,
item_config: None, item_config: None,
}; };
item.update_item_config(); item.update_item_config(msm);
item item
} }
@ -413,7 +405,8 @@ impl Item {
/// Panics if the asset does not exist. /// Panics if the asset does not exist.
pub fn new_from_asset_expect(asset_specifier: &str) -> Self { pub fn new_from_asset_expect(asset_specifier: &str) -> Self {
let inner_item = Arc::<ItemDef>::load_expect_cloned(asset_specifier); let inner_item = Arc::<ItemDef>::load_expect_cloned(asset_specifier);
Item::new_from_item_def(inner_item, &[]) let msm = MaterialStatManifest::default();
Item::new_from_item_def(inner_item, &[], &msm)
} }
/// Creates a Vec containing one of each item that matches the provided /// Creates a Vec containing one of each item that matches the provided
@ -426,12 +419,13 @@ impl Item {
/// it exists /// it exists
pub fn new_from_asset(asset: &str) -> Result<Self, Error> { pub fn new_from_asset(asset: &str) -> Result<Self, Error> {
let inner_item = Arc::<ItemDef>::load_cloned(asset)?; let inner_item = Arc::<ItemDef>::load_cloned(asset)?;
Ok(Item::new_from_item_def(inner_item, &[])) let msm = MaterialStatManifest::default();
Ok(Item::new_from_item_def(inner_item, &[], &msm))
} }
/// Duplicates an item, creating an exact copy but with a new item ID /// Duplicates an item, creating an exact copy but with a new item ID
pub fn duplicate(&self) -> Self { pub fn duplicate(&self, msm: &MaterialStatManifest) -> Self {
Item::new_from_item_def(Arc::clone(&self.item_def), &self.components) Item::new_from_item_def(Arc::clone(&self.item_def), &self.components, msm)
} }
/// FIXME: HACK: In order to set the entity ID asynchronously, we currently /// FIXME: HACK: In order to set the entity ID asynchronously, we currently
@ -494,21 +488,22 @@ impl Item {
} }
} }
pub fn add_component(&mut self, component: Item) { pub fn add_component(&mut self, component: Item, msm: &MaterialStatManifest) {
// TODO: hook for typechecking (not needed atm if this is only used by DB // TODO: hook for typechecking (not needed atm if this is only used by DB
// persistence, but will definitely be needed once enhancement slots are // persistence, but will definitely be needed once enhancement slots are
// added to prevent putting a sword into another sword) // added to prevent putting a sword into another sword)
self.components.push(component); self.components.push(component);
// adding a component changes the stats, so recalculate the ItemConfig // adding a component changes the stats, so recalculate the ItemConfig
self.update_item_config(); self.update_item_config(msm);
} }
fn update_item_config(&mut self) { fn update_item_config(&mut self, msm: &MaterialStatManifest) {
self.item_config = if let ItemKind::Tool(_) = self.kind() { self.item_config = if let ItemKind::Tool(_) = self.kind() {
Some(Box::new(ItemConfig::from(( Some(Box::new(ItemConfig::from((
self.kind(), self.kind(),
self.components(), self.components(),
&self.item_def.ability_map, &self.item_def.ability_map,
msm,
)))) ))))
} else { } else {
None None

View File

@ -2,17 +2,13 @@
// version in voxygen\src\meta.rs in order to reset save files to being empty // version in voxygen\src\meta.rs in order to reset save files to being empty
use crate::{ use crate::{
assets::{self, Asset}, assets::{self, Asset, AssetExt},
comp::{ comp::{item::ItemKind, skills::Skill, CharacterAbility, Item},
item::{ItemDesc, ItemKind, ItemTag},
skills::Skill,
CharacterAbility, Item,
},
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
ops::{AddAssign, MulAssign}, ops::{AddAssign, DivAssign, MulAssign},
time::Duration, time::Duration,
}; };
use tracing::error; use tracing::error;
@ -78,6 +74,12 @@ impl Stats {
} }
} }
impl Asset for Stats {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
impl AddAssign<Stats> for Stats { impl AddAssign<Stats> for Stats {
fn add_assign(&mut self, other: Stats) { fn add_assign(&mut self, other: Stats) {
self.equip_time_millis += other.equip_time_millis; self.equip_time_millis += other.equip_time_millis;
@ -86,11 +88,43 @@ impl AddAssign<Stats> for Stats {
self.speed += other.speed; self.speed += other.speed;
} }
} }
impl MulAssign<f32> for Stats { impl MulAssign<Stats> for Stats {
fn mul_assign(&mut self, scalar: f32) { fn mul_assign(&mut self, other: Stats) {
self.power *= scalar; // equip_time_millis doesn't quite work with mul since it's u32, so we can't
self.poise_strength *= scalar; // scale delay down, only up, so it needs to be balanced carefully in
self.speed *= scalar; // multiplicative contexts
self.equip_time_millis *= other.equip_time_millis;
self.power *= other.power;
self.poise_strength *= other.poise_strength;
self.speed *= other.speed;
}
}
impl DivAssign<usize> for Stats {
fn div_assign(&mut self, scalar: usize) {
self.equip_time_millis /= scalar as u32;
// since averaging occurs when the stats are used multiplicatively, don't permit
// multiplying an equip_time_millis by 0, since that would be overpowered
self.equip_time_millis = self.equip_time_millis.max(1);
self.power /= scalar as f32;
self.poise_strength /= scalar as f32;
self.speed /= scalar as f32;
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MaterialStatManifest(HashMap<String, Stats>);
// 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";
}
impl Default for MaterialStatManifest {
fn default() -> MaterialStatManifest {
MaterialStatManifest::load_expect_cloned("common.material_stats_manifest")
} }
} }
@ -101,38 +135,36 @@ pub enum StatKind {
} }
impl StatKind { impl StatKind {
pub fn resolve_stats(&self, components: &[Item]) -> Stats { pub fn resolve_stats(&self, msm: &MaterialStatManifest, components: &[Item]) -> Stats {
let mut stats = match self { let mut stats = match self {
StatKind::Direct(stats) => *stats, StatKind::Direct(stats) => *stats,
StatKind::Modular => Stats::zeroed(), StatKind::Modular => Stats::zeroed(),
}; };
let mut best_multiplier: Option<f32> = None; let mut multipliers: Vec<Stats> = Vec::new();
for item in components.iter() { for item in components.iter() {
match item.kind() { match item.kind() {
ItemKind::ModularComponent(mc) => { ItemKind::ModularComponent(mc) => {
let inner_stats = StatKind::Direct(mc.stats).resolve_stats(item.components()); let inner_stats =
StatKind::Direct(mc.stats).resolve_stats(msm, item.components());
stats += inner_stats; stats += inner_stats;
}, },
ItemKind::Ingredient { .. } => { ItemKind::Ingredient { .. } => {
for tag in item.tags() { if let Some(mult_stats) = msm.0.get(item.item_definition_id()) {
// exhaustive match to ensure that new tags get stats counted multipliers.push(*mult_stats);
match tag {
ItemTag::ClothItem => {},
ItemTag::ModularComponent(_) => {},
ItemTag::MetalIngot(multiplier) => {
best_multiplier =
Some(best_multiplier.unwrap_or(*multiplier).max(*multiplier));
},
}
} }
}, },
// TODO: add stats from enhancement slots, unless those end up as tagged // TODO: add stats from enhancement slots
// ingredients
_ => (), _ => (),
} }
} }
if let Some(multiplier) = best_multiplier { // Take the average of the material multipliers, to allow alloyed blades
stats *= multiplier; if !multipliers.is_empty() {
let mut average_mult = Stats::zeroed();
for stat in multipliers.iter() {
average_mult += *stat;
}
average_mult /= multipliers.len();
stats *= average_mult;
} }
// if an item has 0.0 speed, that panics due to being infinite duration, so // if an item has 0.0 speed, that panics due to being infinite duration, so
// enforce speed >= 0.1 // enforce speed >= 0.1
@ -141,9 +173,9 @@ impl StatKind {
} }
} }
impl From<(&[Item], &Tool)> for Stats { impl From<(&MaterialStatManifest, &[Item], &Tool)> for Stats {
fn from((components, tool): (&[Item], &Tool)) -> Self { fn from((msm, components, tool): (&MaterialStatManifest, &[Item], &Tool)) -> Self {
let raw_stats = tool.stats.resolve_stats(components); let raw_stats = tool.stats.resolve_stats(msm, components);
let (power, speed) = match tool.hands { let (power, speed) = match tool.hands {
Hands::One => (0.67, 1.33), Hands::One => (0.67, 1.33),
// TODO: Restore this when one-handed weapons are made accessible // TODO: Restore this when one-handed weapons are made accessible
@ -204,29 +236,30 @@ impl Tool {
} }
// Keep power between 0.5 and 2.00 // Keep power between 0.5 and 2.00
pub fn base_power(&self, components: &[Item]) -> f32 { pub fn base_power(&self, msm: &MaterialStatManifest, components: &[Item]) -> f32 {
self.stats.resolve_stats(components).power self.stats.resolve_stats(msm, components).power
} }
pub fn base_poise_strength(&self, components: &[Item]) -> f32 { pub fn base_poise_strength(&self, msm: &MaterialStatManifest, components: &[Item]) -> f32 {
self.stats.resolve_stats(components).poise_strength self.stats.resolve_stats(msm, components).poise_strength
} }
pub fn base_speed(&self, components: &[Item]) -> f32 { pub fn base_speed(&self, msm: &MaterialStatManifest, components: &[Item]) -> f32 {
self.stats.resolve_stats(components).speed self.stats.resolve_stats(msm, components).speed
} }
pub fn equip_time(&self, components: &[Item]) -> Duration { pub fn equip_time(&self, msm: &MaterialStatManifest, components: &[Item]) -> Duration {
Duration::from_millis(self.stats.resolve_stats(components).equip_time_millis as u64) Duration::from_millis(self.stats.resolve_stats(msm, components).equip_time_millis as u64)
} }
pub fn get_abilities( pub fn get_abilities(
&self, &self,
msm: &MaterialStatManifest,
components: &[Item], components: &[Item],
map: &AbilityMap, map: &AbilityMap,
) -> AbilitySet<CharacterAbility> { ) -> AbilitySet<CharacterAbility> {
if let Some(set) = map.0.get(&self.kind).cloned() { if let Some(set) = map.0.get(&self.kind).cloned() {
set.modified_by_tool(&self, components) set.modified_by_tool(&self, msm, components)
} else { } else {
error!( error!(
"ToolKind: {:?} has no AbilitySet in the ability map falling back to default", "ToolKind: {:?} has no AbilitySet in the ability map falling back to default",
@ -245,8 +278,13 @@ pub struct AbilitySet<T> {
} }
impl AbilitySet<CharacterAbility> { impl AbilitySet<CharacterAbility> {
pub fn modified_by_tool(self, tool: &Tool, components: &[Item]) -> Self { pub fn modified_by_tool(
let stats = Stats::from((components, tool)); self,
tool: &Tool,
msm: &MaterialStatManifest,
components: &[Item],
) -> Self {
let stats = Stats::from((msm, components, tool));
self.map(|a| a.adjusted_by_stats(stats.power, stats.poise_strength, stats.speed)) self.map(|a| a.adjusted_by_stats(stats.power, stats.poise_strength, stats.speed))
} }
} }

View File

@ -9,7 +9,7 @@ use tracing::{debug, trace, warn};
use crate::{ use crate::{
comp::{ comp::{
inventory::{ inventory::{
item::ItemDef, item::{ItemDef, MaterialStatManifest},
loadout::Loadout, loadout::Loadout,
slot::{EquipSlot, Slot, SlotError}, slot::{EquipSlot, Slot, SlotError},
}, },
@ -257,9 +257,9 @@ impl Inventory {
} }
/// Remove just one item from the slot /// Remove just one item from the slot
pub fn take(&mut self, inv_slot_id: InvSlotId) -> Option<Item> { pub fn take(&mut self, inv_slot_id: InvSlotId, msm: &MaterialStatManifest) -> Option<Item> {
if let Some(Some(item)) = self.slot_mut(inv_slot_id) { if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
let mut return_item = item.duplicate(); let mut return_item = item.duplicate(msm);
if item.is_stackable() && item.amount() > 1 { if item.is_stackable() && item.amount() > 1 {
item.decrease_amount(1).ok()?; item.decrease_amount(1).ok()?;

View File

@ -3,7 +3,7 @@ use crate::comp::{
armor, armor,
armor::{ArmorKind, Protection}, armor::{ArmorKind, Protection},
tool::AbilityMap, tool::AbilityMap,
ItemDef, ItemKind, Quality, ItemDef, ItemKind, MaterialStatManifest, Quality,
}, },
Item, Item,
}; };
@ -23,5 +23,5 @@ pub(super) fn get_test_bag(slots: u16) -> Item {
AbilityMap::default(), AbilityMap::default(),
); );
Item::new_from_item_def(Arc::new(item_def), &[]) Item::new_from_item_def(Arc::new(item_def), &[], &MaterialStatManifest::default())
} }

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
assets::{self, AssetExt, AssetHandle}, assets::{self, AssetExt, AssetHandle},
comp::{ comp::{
item::{modular, ItemDef, ItemTag}, item::{modular, ItemDef, ItemTag, MaterialStatManifest},
Inventory, Item, Inventory, Item,
}, },
}; };
@ -27,6 +27,7 @@ impl Recipe {
pub fn perform( pub fn perform(
&self, &self,
inv: &mut Inventory, inv: &mut Inventory,
msm: &MaterialStatManifest,
) -> Result<Option<(Item, u32)>, Vec<(&RecipeInput, u32)>> { ) -> Result<Option<(Item, u32)>, Vec<(&RecipeInput, u32)>> {
// Get ingredient cells from inventory, // Get ingredient cells from inventory,
let mut components = Vec::new(); let mut components = Vec::new();
@ -35,13 +36,16 @@ impl Recipe {
.into_iter() .into_iter()
.for_each(|(pos, n)| { .for_each(|(pos, n)| {
(0..n).for_each(|_| { (0..n).for_each(|_| {
let component = inv.take(pos).expect("Expected item to exist in inventory"); let component = inv
.take(pos, msm)
.expect("Expected item to exist in inventory");
components.push(component); components.push(component);
}) })
}); });
for i in 0..self.output.1 { for i in 0..self.output.1 {
let crafted_item = Item::new_from_item_def(Arc::clone(&self.output.0), &components); let crafted_item =
Item::new_from_item_def(Arc::clone(&self.output.0), &components, msm);
if let Some(item) = inv.push(crafted_item) { if let Some(item) = inv.push(crafted_item) {
return Ok(Some((item, self.output.1 - i))); return Ok(Some((item, self.output.1 - i)));
} }

View File

@ -1,7 +1,8 @@
use crate::{ use crate::{
comp::{ comp::{
Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy, Health, item::MaterialStatManifest, Beam, Body, CharacterState, ControlAction, Controller,
Inventory, LoadoutManip, Melee, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel, ControllerInputs, Energy, Health, Inventory, LoadoutManip, Melee, Ori, PhysicsState, Pos,
StateUpdate, Stats, Vel,
}, },
resources::DeltaTime, resources::DeltaTime,
uid::Uid, uid::Uid,
@ -66,6 +67,7 @@ pub struct JoinData<'a> {
pub melee_attack: Option<&'a Melee>, pub melee_attack: Option<&'a Melee>,
pub updater: &'a LazyUpdate, pub updater: &'a LazyUpdate,
pub stats: &'a Stats, pub stats: &'a Stats,
pub msm: &'a MaterialStatManifest,
} }
type RestrictedMut<'a, C> = PairedStorage< type RestrictedMut<'a, C> = PairedStorage<
@ -96,7 +98,12 @@ pub struct JoinStruct<'a> {
} }
impl<'a> JoinData<'a> { impl<'a> JoinData<'a> {
pub fn new(j: &'a JoinStruct<'a>, updater: &'a LazyUpdate, dt: &'a DeltaTime) -> Self { pub fn new(
j: &'a JoinStruct<'a>,
updater: &'a LazyUpdate,
dt: &'a DeltaTime,
msm: &'a MaterialStatManifest,
) -> Self {
Self { Self {
entity: j.entity, entity: j.entity,
uid: j.uid, uid: j.uid,
@ -115,6 +122,7 @@ impl<'a> JoinData<'a> {
stats: j.stat, stats: j.stat,
updater, updater,
dt, dt,
msm,
} }
} }
} }

View File

@ -301,7 +301,7 @@ pub fn attempt_wield(data: &JoinData, update: &mut StateUpdate) {
{ {
update.character = CharacterState::Equipping(equipping::Data { update.character = CharacterState::Equipping(equipping::Data {
static_data: equipping::StaticData { static_data: equipping::StaticData {
buildup_duration: tool.equip_time(item.components()), buildup_duration: tool.equip_time(data.msm, item.components()),
}, },
timer: Duration::default(), timer: Duration::default(),
}); });

View File

@ -5,7 +5,10 @@ use specs::{
use common::{ use common::{
comp::{ comp::{
inventory::slot::{EquipSlot, Slot}, inventory::{
item::MaterialStatManifest,
slot::{EquipSlot, Slot},
},
Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Melee, Mounting, Ori, Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Melee, Mounting, Ori,
PhysicsState, Poise, PoiseState, Pos, StateUpdate, Stats, Vel, PhysicsState, Poise, PoiseState, Pos, StateUpdate, Stats, Vel,
}, },
@ -62,6 +65,7 @@ pub struct ReadData<'a> {
uids: ReadStorage<'a, Uid>, uids: ReadStorage<'a, Uid>,
mountings: ReadStorage<'a, Mounting>, mountings: ReadStorage<'a, Mounting>,
stats: ReadStorage<'a, Stats>, stats: ReadStorage<'a, Stats>,
msm: Read<'a, MaterialStatManifest>,
} }
/// ## Character Behavior System /// ## Character Behavior System
@ -252,7 +256,12 @@ impl<'a> System<'a> for Sys {
}; };
for action in actions { for action in actions {
let j = JoinData::new(&join_struct, &read_data.lazy_update, &read_data.dt); let j = JoinData::new(
&join_struct,
&read_data.lazy_update,
&read_data.dt,
&read_data.msm,
);
let mut state_update = match j.character { let mut state_update = match j.character {
CharacterState::Idle => states::idle::Data.handle_event(&j, action), CharacterState::Idle => states::idle::Data.handle_event(&j, action),
CharacterState::Talk => states::talk::Data.handle_event(&j, action), CharacterState::Talk => states::talk::Data.handle_event(&j, action),
@ -295,7 +304,12 @@ impl<'a> System<'a> for Sys {
incorporate_update(&mut join_struct, state_update); incorporate_update(&mut join_struct, state_update);
} }
let j = JoinData::new(&join_struct, &read_data.lazy_update, &read_data.dt); let j = JoinData::new(
&join_struct,
&read_data.lazy_update,
&read_data.dt,
&read_data.msm,
);
let mut state_update = match j.character { let mut state_update = match j.character {
CharacterState::Idle => states::idle::Data.behavior(&j), CharacterState::Idle => states::idle::Data.behavior(&j),

View File

@ -196,6 +196,7 @@ impl State {
ecs.insert(EventBus::<LocalEvent>::default()); ecs.insert(EventBus::<LocalEvent>::default());
ecs.insert(game_mode); ecs.insert(game_mode);
ecs.insert(Vec::<common::outcome::Outcome>::new()); ecs.insert(Vec::<common::outcome::Outcome>::new());
ecs.insert(comp::inventory::item::MaterialStatManifest::default());
// TODO: only register on the server // TODO: only register on the server
ecs.insert(EventBus::<ServerEvent>::default()); ecs.insert(EventBus::<ServerEvent>::default());
ecs.insert(comp::group::GroupManager::default()); ecs.insert(comp::group::GroupManager::default());

View File

@ -13,6 +13,7 @@ use common::{
self, self,
aura::{Aura, AuraKind, AuraTarget}, aura::{Aura, AuraKind, AuraTarget},
buff::{BuffCategory, BuffData, BuffKind, BuffSource}, buff::{BuffCategory, BuffData, BuffKind, BuffSource},
inventory::item::MaterialStatManifest,
invite::InviteKind, invite::InviteKind,
ChatType, Inventory, Item, LightEmitter, WaypointArea, ChatType, Inventory, Item, LightEmitter, WaypointArea,
}, },
@ -205,6 +206,7 @@ fn handle_give_item(
} }
}); });
} else { } else {
let msm = server.state.ecs().read_resource::<MaterialStatManifest>();
// This item can't stack. Give each item in a loop. // This item can't stack. Give each item in a loop.
server server
.state .state
@ -213,7 +215,7 @@ fn handle_give_item(
.get_mut(target) .get_mut(target)
.map(|mut inv| { .map(|mut inv| {
for i in 0..give_amount { for i in 0..give_amount {
if inv.push(item.duplicate()).is_some() { if inv.push(item.duplicate(&msm)).is_some() {
server.notify_client( server.notify_client(
client, client,
ServerGeneral::server_msg( ServerGeneral::server_msg(

View File

@ -13,6 +13,7 @@ use common::{
comp::{ comp::{
self, aura, buff, self, aura, buff,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
inventory::item::MaterialStatManifest,
object, Alignment, Body, CharacterState, Energy, EnergyChange, Group, Health, HealthChange, object, Alignment, Body, CharacterState, Energy, EnergyChange, Group, Health, HealthChange,
HealthSource, Inventory, Item, Player, Poise, PoiseChange, PoiseSource, Pos, Stats, HealthSource, Inventory, Item, Player, Poise, PoiseChange, PoiseSource, Pos, Stats,
}, },
@ -224,9 +225,14 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
const MAX_EXP_DIST: f32 = 150.0; const MAX_EXP_DIST: f32 = 150.0;
// TODO: Scale xp from skillset rather than health, when NPCs have their own // TODO: Scale xp from skillset rather than health, when NPCs have their own
// skillsets // skillsets
let mut exp_reward = let msm = state.ecs().read_resource::<MaterialStatManifest>();
combat::combat_rating(entity_inventory, entity_health, entity_stats, *entity_body) let mut exp_reward = combat::combat_rating(
* 2.5; entity_inventory,
entity_health,
entity_stats,
*entity_body,
&msm,
) * 2.5;
// Distribute EXP to group // Distribute EXP to group
let positions = state.ecs().read_storage::<Pos>(); let positions = state.ecs().read_storage::<Pos>();

View File

@ -260,7 +260,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo
})); }));
} }
Some(comp::InventoryUpdateEvent::Used) Some(comp::InventoryUpdateEvent::Used)
} else if let Some(item) = inventory.take(slot) { } else if let Some(item) = inventory.take(
slot,
&state.ecs().read_resource::<item::MaterialStatManifest>(),
) {
match item.kind() { match item.kind() {
ItemKind::Consumable { kind, effect, .. } => { ItemKind::Consumable { kind, effect, .. } => {
maybe_effect = Some(effect.clone()); maybe_effect = Some(effect.clone());
@ -484,9 +487,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo
.get_mut(entity) .get_mut(entity)
{ {
let recipe_book = default_recipe_book().read(); let recipe_book = default_recipe_book().read();
let craft_result = recipe_book let craft_result = recipe_book.get(&recipe).and_then(|r| {
.get(&recipe) r.perform(
.and_then(|r| r.perform(&mut inv).ok()); &mut inv,
&state.ecs().read_resource::<item::MaterialStatManifest>(),
)
.ok()
});
// FIXME: We should really require the drop and write to be atomic! // FIXME: We should really require the drop and write to be atomic!
if craft_result.is_some() { if craft_result.is_some() {

View File

@ -1,6 +1,6 @@
use crate::Server; use crate::Server;
use common::{ use common::{
comp::inventory::Inventory, comp::inventory::{item::MaterialStatManifest, Inventory},
trade::{PendingTrade, TradeAction, TradeId, TradeResult, Trades}, trade::{PendingTrade, TradeAction, TradeId, TradeResult, Trades},
}; };
use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
@ -116,6 +116,7 @@ fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
} }
} }
let mut items = [Vec::new(), Vec::new()]; let mut items = [Vec::new(), Vec::new()];
let msm = ecs.read_resource::<MaterialStatManifest>();
for who in [0, 1].iter().cloned() { for who in [0, 1].iter().cloned() {
for (slot, quantity) in trade.offers[who].iter() { for (slot, quantity) in trade.offers[who].iter() {
// Take the items one by one, to benefit from Inventory's stack handling // Take the items one by one, to benefit from Inventory's stack handling
@ -123,7 +124,7 @@ fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
inventories inventories
.get_mut(entities[who]) .get_mut(entities[who])
.expect(invmsg) .expect(invmsg)
.take(*slot) .take(*slot, &msm)
.map(|item| items[who].push(item)); .map(|item| items[who].push(item));
} }
} }

View File

@ -58,7 +58,7 @@ use common::{
assets::AssetExt, assets::AssetExt,
cmd::ChatCommand, cmd::ChatCommand,
comp, comp,
comp::CharacterAbility, comp::{item::MaterialStatManifest, CharacterAbility},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
recipe::default_recipe_book, recipe::default_recipe_book,
resources::TimeOfDay, resources::TimeOfDay,
@ -954,6 +954,7 @@ impl Server {
client_timeout: self.settings().client_timeout, client_timeout: self.settings().client_timeout,
world_map: self.map.clone(), world_map: self.map.clone(),
recipe_book: default_recipe_book().cloned(), recipe_book: default_recipe_book().cloned(),
material_stats: MaterialStatManifest::default(),
ability_map: (&*self ability_map: (&*self
.state .state
.ecs() .ecs()

View File

@ -9,7 +9,7 @@ extern crate diesel;
use super::{error::Error, models::*, schema, VelorenTransaction}; use super::{error::Error, models::*, schema, VelorenTransaction};
use crate::{ use crate::{
comp, comp,
comp::Inventory, comp::{item::MaterialStatManifest, Inventory},
persistence::{ persistence::{
character::conversions::{ character::conversions::{
convert_body_from_database, convert_body_to_database_json, convert_body_from_database, convert_body_to_database_json,
@ -78,6 +78,7 @@ pub fn load_character_data(
requesting_player_uuid: String, requesting_player_uuid: String,
char_id: CharacterId, char_id: CharacterId,
connection: VelorenTransaction, connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterDataResult { ) -> CharacterDataResult {
use schema::{body::dsl::*, character::dsl::*, skill_group::dsl::*}; use schema::{body::dsl::*, character::dsl::*, skill_group::dsl::*};
@ -127,6 +128,7 @@ pub fn load_character_data(
&inventory_items, &inventory_items,
character_containers.loadout_container_id, character_containers.loadout_container_id,
&loadout_items, &loadout_items,
msm,
)?, )?,
char_waypoint, char_waypoint,
)) ))
@ -142,6 +144,7 @@ pub fn load_character_data(
pub fn load_character_list( pub fn load_character_list(
player_uuid_: &str, player_uuid_: &str,
connection: VelorenTransaction, connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterListResult { ) -> CharacterListResult {
use schema::{body::dsl::*, character::dsl::*}; use schema::{body::dsl::*, character::dsl::*};
@ -170,7 +173,7 @@ pub fn load_character_list(
let loadout_items = load_items_bfs(connection, loadout_container_id)?; let loadout_items = load_items_bfs(connection, loadout_container_id)?;
let loadout = let loadout =
convert_loadout_from_database_items(loadout_container_id, &loadout_items)?; convert_loadout_from_database_items(loadout_container_id, &loadout_items, msm)?;
Ok(CharacterItem { Ok(CharacterItem {
character: char, character: char,
@ -186,6 +189,7 @@ pub fn create_character(
character_alias: &str, character_alias: &str,
persisted_components: PersistedComponents, persisted_components: PersistedComponents,
connection: VelorenTransaction, connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterCreationResult { ) -> CharacterCreationResult {
use schema::item::dsl::*; use schema::item::dsl::*;
@ -317,7 +321,7 @@ pub fn create_character(
))); )));
} }
load_character_list(uuid, connection).map(|list| (character_id, list)) load_character_list(uuid, connection, msm).map(|list| (character_id, list))
} }
/// Delete a character. Returns the updated character list. /// Delete a character. Returns the updated character list.
@ -325,6 +329,7 @@ pub fn delete_character(
requesting_player_uuid: &str, requesting_player_uuid: &str,
char_id: CharacterId, char_id: CharacterId,
connection: VelorenTransaction, connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterListResult { ) -> CharacterListResult {
use schema::{body::dsl::*, character::dsl::*, skill::dsl::*, skill_group::dsl::*}; use schema::{body::dsl::*, character::dsl::*, skill::dsl::*, skill_group::dsl::*};
@ -402,7 +407,7 @@ pub fn delete_character(
))); )));
} }
load_character_list(requesting_player_uuid, connection) load_character_list(requesting_player_uuid, connection, msm)
} }
/// Before creating a character, we ensure that the limit on the number of /// Before creating a character, we ensure that the limit on the number of

View File

@ -11,6 +11,7 @@ use common::{
character::CharacterId, character::CharacterId,
comp::{ comp::{
inventory::{ inventory::{
item::MaterialStatManifest,
loadout::{Loadout, LoadoutError}, loadout::{Loadout, LoadoutError},
loadout_builder::LoadoutBuilder, loadout_builder::LoadoutBuilder,
slot::InvSlotId, slot::InvSlotId,
@ -225,6 +226,7 @@ pub fn convert_inventory_from_database_items(
inventory_items: &[Item], inventory_items: &[Item],
loadout_container_id: i64, loadout_container_id: i64,
loadout_items: &[Item], loadout_items: &[Item],
msm: &MaterialStatManifest,
) -> Result<Inventory, Error> { ) -> Result<Inventory, Error> {
// Loadout items must be loaded before inventory items since loadout items // Loadout items must be loaded before inventory items since loadout items
// provide inventory slots. Since items stored inside loadout items actually // provide inventory slots. Since items stored inside loadout items actually
@ -232,7 +234,7 @@ pub fn convert_inventory_from_database_items(
// on populating the loadout items first, and then inserting the items into the // on populating the loadout items first, and then inserting the items into the
// inventory at the correct position. // inventory at the correct position.
// //
let loadout = convert_loadout_from_database_items(loadout_container_id, loadout_items)?; let loadout = convert_loadout_from_database_items(loadout_container_id, loadout_items, msm)?;
let mut inventory = Inventory::new_with_loadout(loadout); let mut inventory = Inventory::new_with_loadout(loadout);
let mut item_indices = HashMap::new(); let mut item_indices = HashMap::new();
@ -294,7 +296,7 @@ pub fn convert_inventory_from_database_items(
} }
} else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) { } else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) {
if let Some(Some(parent)) = inventory.slot_mut(slot(&inventory_items[j].position)?) { if let Some(Some(parent)) = inventory.slot_mut(slot(&inventory_items[j].position)?) {
parent.add_component(item); parent.add_component(item, msm);
} else { } else {
return Err(Error::ConversionError(format!( return Err(Error::ConversionError(format!(
"Parent slot {} for component {} was empty even though it occurred earlier in \ "Parent slot {} for component {} was empty even though it occurred earlier in \
@ -316,6 +318,7 @@ pub fn convert_inventory_from_database_items(
pub fn convert_loadout_from_database_items( pub fn convert_loadout_from_database_items(
loadout_container_id: i64, loadout_container_id: i64,
database_items: &[Item], database_items: &[Item],
msm: &MaterialStatManifest,
) -> Result<Loadout, Error> { ) -> Result<Loadout, Error> {
let loadout_builder = LoadoutBuilder::new(); let loadout_builder = LoadoutBuilder::new();
let mut loadout = loadout_builder.build(); let mut loadout = loadout_builder.build();
@ -348,7 +351,7 @@ pub fn convert_loadout_from_database_items(
} else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) { } else if let Some(&j) = item_indices.get(&db_item.parent_container_item_id) {
loadout loadout
.update_item_at_slot_using_persistence_key(&database_items[j].position, |parent| { .update_item_at_slot_using_persistence_key(&database_items[j].position, |parent| {
parent.add_component(item); parent.add_component(item, msm);
}) })
.map_err(convert_error)?; .map_err(convert_error)?;
} else { } else {

View File

@ -3,8 +3,12 @@ use crate::persistence::{
error::Error, error::Error,
establish_connection, PersistedComponents, establish_connection, PersistedComponents,
}; };
use common::character::{CharacterId, CharacterItem}; use common::{
character::{CharacterId, CharacterItem},
comp::inventory::item::MaterialStatManifest,
};
use crossbeam_channel::{self, TryIter}; use crossbeam_channel::{self, TryIter};
use lazy_static::lazy_static;
use std::path::Path; use std::path::Path;
use tracing::error; use tracing::error;
@ -66,6 +70,13 @@ pub struct CharacterLoader {
update_tx: crossbeam_channel::Sender<CharacterLoaderRequest>, update_tx: crossbeam_channel::Sender<CharacterLoaderRequest>,
} }
// Decoupled from the ECS resource because the plumbing is getting complicated;
// shouldn't matter unless someone's hot-reloading material stats on the live
// server
lazy_static! {
pub static ref MATERIAL_STATS_MANIFEST: MaterialStatManifest = MaterialStatManifest::default();
}
impl CharacterLoader { impl CharacterLoader {
pub fn new(db_dir: &Path) -> diesel::QueryResult<Self> { pub fn new(db_dir: &Path) -> diesel::QueryResult<Self> {
let (update_tx, internal_rx) = crossbeam_channel::unbounded::<CharacterLoaderRequest>(); let (update_tx, internal_rx) = crossbeam_channel::unbounded::<CharacterLoaderRequest>();
@ -93,6 +104,7 @@ impl CharacterLoader {
&character_alias, &character_alias,
persisted_components, persisted_components,
txn, txn,
&MATERIAL_STATS_MANIFEST,
) )
}, },
)), )),
@ -100,19 +112,37 @@ impl CharacterLoader {
player_uuid, player_uuid,
character_id, character_id,
} => CharacterLoaderResponseKind::CharacterList(conn.transaction( } => CharacterLoaderResponseKind::CharacterList(conn.transaction(
|txn| delete_character(&player_uuid, character_id, txn), |txn| {
delete_character(
&player_uuid,
character_id,
txn,
&MATERIAL_STATS_MANIFEST,
)
},
)), )),
CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => { CharacterLoaderRequestKind::LoadCharacterList { player_uuid } => {
CharacterLoaderResponseKind::CharacterList( CharacterLoaderResponseKind::CharacterList(conn.transaction(
conn.transaction(|txn| load_character_list(&player_uuid, txn)), |txn| {
) load_character_list(
&player_uuid,
txn,
&MATERIAL_STATS_MANIFEST,
)
},
))
}, },
CharacterLoaderRequestKind::LoadCharacterData { CharacterLoaderRequestKind::LoadCharacterData {
player_uuid, player_uuid,
character_id, character_id,
} => { } => {
let result = conn.transaction(|txn| { let result = conn.transaction(|txn| {
load_character_data(player_uuid, character_id, txn) load_character_data(
player_uuid,
character_id,
txn,
&MATERIAL_STATS_MANIFEST,
)
}); });
if result.is_err() { if result.is_err() {
error!( error!(

View File

@ -18,7 +18,10 @@ use crate::{
use client::Client; use client::Client;
use common::{ use common::{
combat::{combat_rating, Damage}, combat::{combat_rating, Damage},
comp::{item::Quality, Body, Energy, Health, Stats}, comp::{
item::{MaterialStatManifest, Quality},
Body, Energy, Health, Stats,
},
}; };
use conrod_core::{ use conrod_core::{
color, color,
@ -101,6 +104,7 @@ pub struct Bag<'a> {
energy: &'a Energy, energy: &'a Energy,
show: &'a Show, show: &'a Show,
body: &'a Body, body: &'a Body,
msm: &'a MaterialStatManifest,
} }
impl<'a> Bag<'a> { impl<'a> Bag<'a> {
@ -120,6 +124,7 @@ impl<'a> Bag<'a> {
energy: &'a Energy, energy: &'a Energy,
show: &'a Show, show: &'a Show,
body: &'a Body, body: &'a Body,
msm: &'a MaterialStatManifest,
) -> Self { ) -> Self {
Self { Self {
client, client,
@ -137,6 +142,7 @@ impl<'a> Bag<'a> {
health, health,
show, show,
body, body,
msm,
} }
} }
} }
@ -433,7 +439,7 @@ impl<'a> Widget for Bag<'a> {
}); });
// Stats // Stats
let combat_rating = let combat_rating =
combat_rating(inventory, self.health, self.stats, *self.body).min(999.9); combat_rating(inventory, self.health, self.stats, *self.body, &self.msm).min(999.9);
let indicator_col = cr_color(combat_rating); let indicator_col = cr_color(combat_rating);
for i in STATS.iter().copied().enumerate() { for i in STATS.iter().copied().enumerate() {
let btn = Button::image(match i.1 { let btn = Button::image(match i.1 {

View File

@ -16,7 +16,9 @@ use crate::{
use client::{self, Client}; use client::{self, Client};
use common::{ use common::{
combat, combat,
comp::{group::Role, invite::InviteKind, BuffKind, Stats}, comp::{
group::Role, inventory::item::MaterialStatManifest, invite::InviteKind, BuffKind, Stats,
},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
}; };
use common_net::sync::WorldSyncExt; use common_net::sync::WorldSyncExt;
@ -80,6 +82,7 @@ pub struct Group<'a> {
pulse: f32, pulse: f32,
global_state: &'a GlobalState, global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager, tooltip_manager: &'a mut TooltipManager,
msm: &'a MaterialStatManifest,
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
@ -98,6 +101,7 @@ impl<'a> Group<'a> {
pulse: f32, pulse: f32,
global_state: &'a GlobalState, global_state: &'a GlobalState,
tooltip_manager: &'a mut TooltipManager, tooltip_manager: &'a mut TooltipManager,
msm: &'a MaterialStatManifest,
) -> Self { ) -> Self {
Self { Self {
show, show,
@ -110,6 +114,7 @@ impl<'a> Group<'a> {
pulse, pulse,
global_state, global_state,
tooltip_manager, tooltip_manager,
msm,
common: widget::CommonBuilder::default(), common: widget::CommonBuilder::default(),
} }
} }
@ -358,7 +363,8 @@ impl<'a> Widget for Group<'a> {
if let (Some(stats), Some(inventory), Some(health), Some(body)) = if let (Some(stats), Some(inventory), Some(health), Some(body)) =
(stats, inventory, health, body) (stats, inventory, health, body)
{ {
let combat_rating = combat::combat_rating(inventory, health, stats, *body); let combat_rating =
combat::combat_rating(inventory, health, stats, *body, &self.msm);
let char_name = stats.name.to_string(); let char_name = stats.name.to_string();
let health_perc = health.current() as f64 / health.maximum() as f64; let health_perc = health.current() as f64 / health.maximum() as f64;
// change panel positions when debug info is shown // change panel positions when debug info is shown

View File

@ -66,7 +66,7 @@ use common::{
combat, combat,
comp::{ comp::{
self, self,
item::{tool::ToolKind, ItemDesc, Quality}, item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
skills::{Skill, SkillGroupKind}, skills::{Skill, SkillGroupKind},
BuffKind, BuffKind,
}, },
@ -875,6 +875,7 @@ impl Hud {
let bodies = ecs.read_storage::<comp::Body>(); let bodies = ecs.read_storage::<comp::Body>();
let items = ecs.read_storage::<comp::Item>(); let items = ecs.read_storage::<comp::Item>();
let inventories = ecs.read_storage::<comp::Inventory>(); let inventories = ecs.read_storage::<comp::Inventory>();
let msm = ecs.read_resource::<MaterialStatManifest>();
let entities = ecs.entities(); let entities = ecs.entities();
let me = client.entity(); let me = client.entity();
@ -1370,7 +1371,9 @@ impl Hud {
health, health,
buffs, buffs,
energy, energy,
combat_rating: combat::combat_rating(inventory, health, stats, *body), combat_rating: combat::combat_rating(
inventory, health, stats, *body, &msm,
),
}); });
let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) { let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) {
speech_bubbles.get(uid) speech_bubbles.get(uid)
@ -1932,6 +1935,7 @@ impl Hud {
let ecs = client.state().ecs(); let ecs = client.state().ecs();
let stats = ecs.read_storage::<comp::Stats>(); let stats = ecs.read_storage::<comp::Stats>();
let buffs = ecs.read_storage::<comp::Buffs>(); let buffs = ecs.read_storage::<comp::Buffs>();
let msm = ecs.read_resource::<MaterialStatManifest>();
if let Some(player_stats) = stats.get(client.entity()) { if let Some(player_stats) = stats.get(client.entity()) {
match Buttons::new( match Buttons::new(
client, client,
@ -1968,6 +1972,7 @@ impl Hud {
self.pulse, self.pulse,
&global_state, &global_state,
tooltip_manager, tooltip_manager,
&msm,
) )
.set(self.ids.group_window, ui_widgets) .set(self.ids.group_window, ui_widgets)
{ {
@ -2080,6 +2085,7 @@ impl Hud {
&mut self.slot_manager, &mut self.slot_manager,
i18n, i18n,
&ability_map, &ability_map,
&msm,
) )
.set(self.ids.skillbar, ui_widgets); .set(self.ids.skillbar, ui_widgets);
} }
@ -2106,6 +2112,7 @@ impl Hud {
&energy, &energy,
&self.show, &self.show,
&body, &body,
&msm,
) )
.set(self.ids.bag, ui_widgets) .set(self.ids.bag, ui_widgets)
{ {

View File

@ -19,7 +19,7 @@ use common::comp::{
inventory::slot::EquipSlot, inventory::slot::EquipSlot,
item::{ item::{
tool::{AbilityMap, Tool, ToolKind}, tool::{AbilityMap, Tool, ToolKind},
Hands, Item, ItemKind, Hands, Item, ItemKind, MaterialStatManifest,
}, },
Energy, Health, Inventory, Energy, Health, Inventory,
}; };
@ -140,6 +140,7 @@ pub struct Skillbar<'a> {
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
ability_map: &'a AbilityMap, ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
} }
impl<'a> Skillbar<'a> { impl<'a> Skillbar<'a> {
@ -161,6 +162,7 @@ impl<'a> Skillbar<'a> {
slot_manager: &'a mut slots::SlotManager, slot_manager: &'a mut slots::SlotManager,
localized_strings: &'a Localization, localized_strings: &'a Localization,
ability_map: &'a AbilityMap, ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
) -> Self { ) -> Self {
Self { Self {
global_state, global_state,
@ -180,6 +182,7 @@ impl<'a> Skillbar<'a> {
slot_manager, slot_manager,
localized_strings, localized_strings,
ability_map, ability_map,
msm,
} }
} }
} }
@ -623,7 +626,7 @@ impl<'a> Widget for Skillbar<'a> {
.image_color(if let Some((item, tool)) = tool { .image_color(if let Some((item, tool)) = tool {
if self.energy.current() if self.energy.current()
>= tool >= tool
.get_abilities(item.components(), self.ability_map) .get_abilities(&self.msm, item.components(), self.ability_map)
.secondary .secondary
.get_energy_cost() .get_energy_cost()
{ {

View File

@ -2,6 +2,7 @@ use super::{
hotbar::{self, Slot as HotbarSlot}, hotbar::{self, Slot as HotbarSlot},
img_ids, img_ids,
item_imgs::{ItemImgs, ItemKey}, item_imgs::{ItemImgs, ItemKey},
util::MATERIAL_STATS_MANIFEST,
}; };
use crate::ui::slot::{self, SlotKey, SumSlot}; use crate::ui::slot::{self, SlotKey, SumSlot};
use common::comp::{ use common::comp::{
@ -130,7 +131,11 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
( (
i, i,
if let Some(skill) = tool if let Some(skill) = tool
.get_abilities(item.components(), ability_map) .get_abilities(
&MATERIAL_STATS_MANIFEST,
item.components(),
ability_map,
)
.abilities .abilities
.get(0) .get(0)
{ {
@ -173,7 +178,11 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
( (
i, i,
if let Some(skill) = tool if let Some(skill) = tool
.get_abilities(item.components(), ability_map) .get_abilities(
&MATERIAL_STATS_MANIFEST,
item.components(),
ability_map,
)
.abilities .abilities
.get(skill_index) .get(skill_index)
{ {

View File

@ -1,10 +1,16 @@
use common::comp::item::{ use common::comp::item::{
armor::{Armor, ArmorKind, Protection}, armor::{Armor, ArmorKind, Protection},
tool::{Hands, StatKind, Tool, ToolKind}, tool::{Hands, StatKind, Tool, ToolKind},
Item, ItemDesc, ItemKind, ModularComponent, Item, ItemDesc, ItemKind, MaterialStatManifest, ModularComponent,
}; };
use lazy_static::lazy_static;
use std::{borrow::Cow, fmt::Write}; use std::{borrow::Cow, fmt::Write};
lazy_static! {
// TODO: even more plumbing
pub static ref MATERIAL_STATS_MANIFEST: MaterialStatManifest = MaterialStatManifest::default();
}
pub fn loadout_slot_text<'a>( pub fn loadout_slot_text<'a>(
item: Option<&'a impl ItemDesc>, item: Option<&'a impl ItemDesc>,
mut empty: impl FnMut() -> (&'a str, &'a str), mut empty: impl FnMut() -> (&'a str, &'a str),
@ -23,10 +29,16 @@ pub fn item_text<'a>(item: &'a impl ItemDesc) -> (&'_ str, Cow<'a, str>) {
ItemKind::Armor(armor) => { ItemKind::Armor(armor) => {
Cow::Owned(armor_desc(armor, item.description(), item.num_slots())) Cow::Owned(armor_desc(armor, item.description(), item.num_slots()))
}, },
ItemKind::Tool(tool) => Cow::Owned(tool_desc(&tool, item.components(), item.description())), ItemKind::Tool(tool) => Cow::Owned(tool_desc(
&tool,
item.components(),
&MATERIAL_STATS_MANIFEST,
item.description(),
)),
ItemKind::ModularComponent(mc) => Cow::Owned(modular_component_desc( ItemKind::ModularComponent(mc) => Cow::Owned(modular_component_desc(
mc, mc,
item.components(), item.components(),
&MATERIAL_STATS_MANIFEST,
item.description(), item.description(),
)), )),
ItemKind::Glider(_glider) => Cow::Owned(glider_desc(item.description())), ItemKind::Glider(_glider) => Cow::Owned(glider_desc(item.description())),
@ -43,10 +55,15 @@ pub fn item_text<'a>(item: &'a impl ItemDesc) -> (&'_ str, Cow<'a, str>) {
} }
// TODO: localization // TODO: localization
fn modular_component_desc(mc: &ModularComponent, components: &[Item], description: &str) -> String { fn modular_component_desc(
mc: &ModularComponent,
components: &[Item],
msm: &MaterialStatManifest,
description: &str,
) -> String {
let mut result = format!( let mut result = format!(
"Modular Component\n\n{:?}\n\n{}", "Modular Component\n\n{:?}\n\n{}",
StatKind::Direct(mc.stats).resolve_stats(components), StatKind::Direct(mc.stats).resolve_stats(msm, components),
description description
); );
if !components.is_empty() { if !components.is_empty() {
@ -119,7 +136,7 @@ fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String {
description description
} }
fn tool_desc(tool: &Tool, components: &[Item], desc: &str) -> String { fn tool_desc(tool: &Tool, components: &[Item], msm: &MaterialStatManifest, desc: &str) -> String {
let kind = match tool.kind { let kind = match tool.kind {
ToolKind::Sword => "Sword", ToolKind::Sword => "Sword",
ToolKind::Axe => "Axe", ToolKind::Axe => "Axe",
@ -136,13 +153,13 @@ fn tool_desc(tool: &Tool, components: &[Item], desc: &str) -> String {
}; };
// Get tool stats // Get tool stats
let power = tool.base_power(components); let power = tool.base_power(msm, components);
//let poise_strength = tool.base_poise_strength(); //let poise_strength = tool.base_poise_strength();
let hands = match tool.hands { let hands = match tool.hands {
Hands::One => "One", Hands::One => "One",
Hands::Two => "Two", Hands::Two => "Two",
}; };
let speed = tool.base_speed(components); let speed = tool.base_speed(msm, components);
let mut result = format!( let mut result = format!(
"{}-Handed {}\n\nDPS: {:0.1}\n\nPower: {:0.1}\n\nSpeed: {:0.1}\n\n", "{}-Handed {}\n\nDPS: {:0.1}\n\nPower: {:0.1}\n\nSpeed: {:0.1}\n\n",