Merge branch 'aweinstock/modularforms' into 'master'

Support modular weapon components made from a tagged material using the material as a multiplier.

See merge request veloren/veloren!1815
This commit is contained in:
Samuel Keiffer 2021-02-26 00:53:02 +00:00
commit 00937a4e8b
40 changed files with 580 additions and 168 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
ItemDef(
name: "Metal sword blade",
description: "A sword blade made of metal.",
kind: ModularComponent((
toolkind: Sword,
modkind: Damage,
stats: (
equip_time_millis: 250,
power: 1.0,
poise_strength: 0.75,
speed: 0.0,
),
)),
quality: Common,
tags: [ModularComponent((toolkind: Sword, modkind: Damage))],
)

View File

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

View File

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

View File

@ -18,6 +18,5 @@ ItemDef(
], ],
), ),
quality: Common, quality: Common,
tags: [ClothItem], tags: [],
) )

View File

@ -0,0 +1,19 @@
ItemDef(
name: "Any metal ingot",
description: "Ingots made from various metals.",
kind: TagExamples(
item_ids: [
"common.items.crafting_ing.bloodsteel_ingot",
"common.items.crafting_ing.bronze_ingot",
"common.items.crafting_ing.cobalt_ingot",
"common.items.crafting_ing.copper_ingot",
"common.items.crafting_ing.iron_ingot",
"common.items.crafting_ing.steel_ingot",
"common.items.crafting_ing.tin_ingot",
],
),
quality: Common,
tags: [],
)

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,6 +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": (
// ("common.items.crafting_ing.modular.damage.sword.metal_blade", 1),
// [
// (Tag(MetalIngot), 5),
// (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,10 +87,11 @@ pub trait TagExampleInfo {
fn exemplar_identifier(&self) -> &'static str; fn exemplar_identifier(&self) -> &'static str;
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum ItemTag { pub enum ItemTag {
ClothItem, ClothItem,
ModularComponent(ModularComponentTag), ModularComponent(ModularComponentTag),
MetalIngot,
} }
impl TagExampleInfo for ItemTag { impl TagExampleInfo for ItemTag {
@ -98,6 +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",
} }
} }
@ -105,6 +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",
} }
} }
} }
@ -212,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,
@ -245,7 +250,7 @@ impl ItemDef {
ItemKind::Tool(tool::Tool { ItemKind::Tool(tool::Tool {
stats: tool::StatKind::Modular, stats: tool::StatKind::Modular,
.. ..
}) }) | ItemKind::ModularComponent(_)
) )
} }
@ -372,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 {
@ -388,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
} }
@ -396,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
@ -409,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
@ -477,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
@ -644,6 +656,7 @@ pub trait ItemDesc {
fn num_slots(&self) -> u16; fn num_slots(&self) -> u16;
fn item_definition_id(&self) -> &str; fn item_definition_id(&self) -> &str;
fn components(&self) -> &[Item]; fn components(&self) -> &[Item];
fn tags(&self) -> &[ItemTag];
} }
impl ItemDesc for Item { impl ItemDesc for Item {
@ -660,6 +673,8 @@ impl ItemDesc for Item {
fn item_definition_id(&self) -> &str { &self.item_def.item_definition_id } fn item_definition_id(&self) -> &str { &self.item_def.item_definition_id }
fn components(&self) -> &[Item] { &self.components } fn components(&self) -> &[Item] { &self.components }
fn tags(&self) -> &[ItemTag] { &self.item_def.tags }
} }
impl ItemDesc for ItemDef { impl ItemDesc for ItemDef {
@ -676,6 +691,8 @@ impl ItemDesc for ItemDef {
fn item_definition_id(&self) -> &str { &self.item_definition_id } fn item_definition_id(&self) -> &str { &self.item_definition_id }
fn components(&self) -> &[Item] { &[] } fn components(&self) -> &[Item] { &[] }
fn tags(&self) -> &[ItemTag] { &self.tags }
} }
impl Component for Item { impl Component for Item {

View File

@ -2,12 +2,15 @@
// 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::{item::ItemKind, skills::Skill, CharacterAbility, Item}, comp::{item::ItemKind, skills::Skill, CharacterAbility, Item},
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::{
ops::{AddAssign, DivAssign, MulAssign},
time::Duration,
};
use tracing::error; use tracing::error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -69,6 +72,67 @@ impl Stats {
speed: 0.0, speed: 0.0,
} }
} }
pub fn clamp_speed(mut self) -> Stats {
// if a tool has 0.0 speed, that panics due to being infinite duration, so
// enforce speed >= 0.1 on the final product (but not the intermediates)
self.speed = self.speed.max(0.1);
self
}
}
impl Asset for Stats {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
impl AddAssign<Stats> for Stats {
fn add_assign(&mut self, other: Stats) {
self.equip_time_millis += other.equip_time_millis;
self.power += other.power;
self.poise_strength += other.poise_strength;
self.speed += other.speed;
}
}
impl MulAssign<Stats> for Stats {
fn mul_assign(&mut self, other: Stats) {
// equip_time_millis doesn't quite work with mul since it's u32, so we can't
// scale delay down, only up, so it needs to be balanced carefully in
// 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(pub 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")
}
} }
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
@ -78,30 +142,44 @@ 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 multipliers: Vec<Stats> = Vec::new();
for item in components.iter() { for item in components.iter() {
if let ItemKind::ModularComponent(mc) = item.kind() { match item.kind() {
stats.equip_time_millis += mc.stats.equip_time_millis; ItemKind::ModularComponent(mc) => {
stats.power += mc.stats.power; let inner_stats =
stats.poise_strength += mc.stats.poise_strength; StatKind::Direct(mc.stats).resolve_stats(msm, item.components());
stats.speed += mc.stats.speed; stats += inner_stats;
},
ItemKind::Ingredient { .. } => {
if let Some(mult_stats) = msm.0.get(item.item_definition_id()) {
multipliers.push(*mult_stats);
}
},
// TODO: add stats from enhancement slots
_ => (),
} }
// TODO: add stats from enhancement slots
} }
// if an item has 0.0 speed, that panics due to being infinite duration, so // Take the average of the material multipliers, to allow alloyed blades
// enforce speed >= 0.1 if !multipliers.is_empty() {
stats.speed = stats.speed.max(0.1); let mut average_mult = Stats::zeroed();
for stat in multipliers.iter() {
average_mult += *stat;
}
average_mult /= multipliers.len();
stats *= average_mult;
}
stats stats
} }
} }
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).clamp_speed();
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
@ -162,29 +240,33 @@ 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)
.clamp_speed()
.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",
@ -203,8 +285,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 {
@ -503,6 +509,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Head)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Head)),
|| (i18n.get("hud.bag.head"), ""), || (i18n.get("hud.bag.head"), ""),
&self.msm,
); );
let head_q_col = inventory let head_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Head)) .equipped(EquipSlot::Armor(ArmorSlot::Head))
@ -525,6 +532,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Neck)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Neck)),
|| (i18n.get("hud.bag.neck"), ""), || (i18n.get("hud.bag.neck"), ""),
&self.msm,
); );
let neck_q_col = inventory let neck_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Neck)) .equipped(EquipSlot::Armor(ArmorSlot::Neck))
@ -548,6 +556,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Chest)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Chest)),
|| (i18n.get("hud.bag.chest"), ""), || (i18n.get("hud.bag.chest"), ""),
&self.msm,
); );
let chest_q_col = inventory let chest_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Chest)) .equipped(EquipSlot::Armor(ArmorSlot::Chest))
@ -570,6 +579,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Shoulders)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Shoulders)),
|| (i18n.get("hud.bag.shoulders"), ""), || (i18n.get("hud.bag.shoulders"), ""),
&self.msm,
); );
let shoulder_q_col = inventory let shoulder_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Shoulders)) .equipped(EquipSlot::Armor(ArmorSlot::Shoulders))
@ -592,6 +602,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Hands)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Hands)),
|| (i18n.get("hud.bag.hands"), ""), || (i18n.get("hud.bag.hands"), ""),
&self.msm,
); );
let chest_q_col = inventory let chest_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Hands)) .equipped(EquipSlot::Armor(ArmorSlot::Hands))
@ -614,6 +625,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Belt)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Belt)),
|| (i18n.get("hud.bag.belt"), ""), || (i18n.get("hud.bag.belt"), ""),
&self.msm,
); );
let belt_q_col = inventory let belt_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Belt)) .equipped(EquipSlot::Armor(ArmorSlot::Belt))
@ -636,6 +648,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Legs)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Legs)),
|| (i18n.get("hud.bag.legs"), ""), || (i18n.get("hud.bag.legs"), ""),
&self.msm,
); );
let legs_q_col = inventory let legs_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Legs)) .equipped(EquipSlot::Armor(ArmorSlot::Legs))
@ -658,6 +671,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring1)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring1)),
|| (i18n.get("hud.bag.ring"), ""), || (i18n.get("hud.bag.ring"), ""),
&self.msm,
); );
let ring_q_col = inventory let ring_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Ring1)) .equipped(EquipSlot::Armor(ArmorSlot::Ring1))
@ -680,6 +694,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring2)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring2)),
|| (i18n.get("hud.bag.ring"), ""), || (i18n.get("hud.bag.ring"), ""),
&self.msm,
); );
let ring2_q_col = inventory let ring2_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Ring2)) .equipped(EquipSlot::Armor(ArmorSlot::Ring2))
@ -702,6 +717,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Back)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Back)),
|| (i18n.get("hud.bag.back"), ""), || (i18n.get("hud.bag.back"), ""),
&self.msm,
); );
let back_q_col = inventory let back_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Back)) .equipped(EquipSlot::Armor(ArmorSlot::Back))
@ -724,6 +740,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Feet)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Feet)),
|| (i18n.get("hud.bag.feet"), ""), || (i18n.get("hud.bag.feet"), ""),
&self.msm,
); );
let foot_q_col = inventory let foot_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Feet)) .equipped(EquipSlot::Armor(ArmorSlot::Feet))
@ -743,9 +760,11 @@ impl<'a> Widget for Bag<'a> {
) )
.set(state.ids.feet_slot, ui); .set(state.ids.feet_slot, ui);
// Lantern // Lantern
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Lantern), || { let (title, desc) = loadout_slot_text(
(i18n.get("hud.bag.lantern"), "") inventory.equipped(EquipSlot::Lantern),
}); || (i18n.get("hud.bag.lantern"), ""),
&self.msm,
);
let lantern_q_col = inventory let lantern_q_col = inventory
.equipped(EquipSlot::Lantern) .equipped(EquipSlot::Lantern)
.map(|item| get_quality_col(item)) .map(|item| get_quality_col(item))
@ -764,9 +783,11 @@ impl<'a> Widget for Bag<'a> {
) )
.set(state.ids.lantern_slot, ui); .set(state.ids.lantern_slot, ui);
// Glider // Glider
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Glider), || { let (title, desc) = loadout_slot_text(
(i18n.get("hud.bag.glider"), "") inventory.equipped(EquipSlot::Glider),
}); || (i18n.get("hud.bag.glider"), ""),
&self.msm,
);
let glider_q_col = inventory let glider_q_col = inventory
.equipped(EquipSlot::Glider) .equipped(EquipSlot::Glider)
.map(|item| get_quality_col(item)) .map(|item| get_quality_col(item))
@ -788,6 +809,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Tabard)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Tabard)),
|| (i18n.get("hud.bag.tabard"), ""), || (i18n.get("hud.bag.tabard"), ""),
&self.msm,
); );
let tabard_q_col = inventory let tabard_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Tabard)) .equipped(EquipSlot::Armor(ArmorSlot::Tabard))
@ -807,9 +829,11 @@ impl<'a> Widget for Bag<'a> {
) )
.set(state.ids.tabard_slot, ui); .set(state.ids.tabard_slot, ui);
// Mainhand/Left-Slot // Mainhand/Left-Slot
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Mainhand), || { let (title, desc) = loadout_slot_text(
(i18n.get("hud.bag.mainhand"), "") inventory.equipped(EquipSlot::Mainhand),
}); || (i18n.get("hud.bag.mainhand"), ""),
&self.msm,
);
let mainhand_q_col = inventory let mainhand_q_col = inventory
.equipped(EquipSlot::Mainhand) .equipped(EquipSlot::Mainhand)
.map(|item| get_quality_col(item)) .map(|item| get_quality_col(item))
@ -828,9 +852,11 @@ impl<'a> Widget for Bag<'a> {
) )
.set(state.ids.mainhand_slot, ui); .set(state.ids.mainhand_slot, ui);
// Offhand/Right-Slot // Offhand/Right-Slot
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Offhand), || { let (title, desc) = loadout_slot_text(
(i18n.get("hud.bag.offhand"), "") inventory.equipped(EquipSlot::Offhand),
}); || (i18n.get("hud.bag.offhand"), ""),
&self.msm,
);
let offhand_q_col = inventory let offhand_q_col = inventory
.equipped(EquipSlot::Offhand) .equipped(EquipSlot::Offhand)
.map(|item| get_quality_col(item)) .map(|item| get_quality_col(item))
@ -853,6 +879,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag1)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag1)),
|| (i18n.get("hud.bag.bag"), ""), || (i18n.get("hud.bag.bag"), ""),
&self.msm,
); );
let bag1_q_col = inventory let bag1_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag1)) .equipped(EquipSlot::Armor(ArmorSlot::Bag1))
@ -879,6 +906,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag2)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag2)),
|| (i18n.get("hud.bag.bag"), ""), || (i18n.get("hud.bag.bag"), ""),
&self.msm,
); );
let bag2_q_col = inventory let bag2_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag2)) .equipped(EquipSlot::Armor(ArmorSlot::Bag2))
@ -901,6 +929,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag3)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag3)),
|| (i18n.get("hud.bag.bag"), ""), || (i18n.get("hud.bag.bag"), ""),
&self.msm,
); );
let bag3_q_col = inventory let bag3_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag3)) .equipped(EquipSlot::Armor(ArmorSlot::Bag3))
@ -923,6 +952,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text( let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag4)), inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag4)),
|| (i18n.get("hud.bag.bag"), ""), || (i18n.get("hud.bag.bag"), ""),
&self.msm,
); );
let bag4_q_col = inventory let bag4_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag4)) .equipped(EquipSlot::Armor(ArmorSlot::Bag4))
@ -1005,7 +1035,7 @@ impl<'a> Widget for Bag<'a> {
} }
if let Some(item) = item { if let Some(item) = item {
let (title, desc) = super::util::item_text(item); let (title, desc) = super::util::item_text(item, &self.msm);
let quality_col = get_quality_col(item); let quality_col = get_quality_col(item);
let quality_col_img = match item.quality() { let quality_col_img = match item.quality() {
Quality::Low => self.imgs.inv_slot_grey, Quality::Low => self.imgs.inv_slot_grey,

View File

@ -12,7 +12,7 @@ use client::{self, Client};
use common::{ use common::{
assets::AssetExt, assets::AssetExt,
comp::{ comp::{
item::{ItemDef, ItemDesc, Quality, TagExampleInfo}, item::{ItemDef, ItemDesc, MaterialStatManifest, Quality, TagExampleInfo},
Inventory, Inventory,
}, },
recipe::RecipeInput, recipe::RecipeInput,
@ -68,6 +68,7 @@ pub struct Crafting<'a> {
tooltip_manager: &'a mut TooltipManager, tooltip_manager: &'a mut TooltipManager,
item_imgs: &'a ItemImgs, item_imgs: &'a ItemImgs,
inventory: &'a Inventory, inventory: &'a Inventory,
msm: &'a MaterialStatManifest,
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
} }
@ -83,6 +84,7 @@ impl<'a> Crafting<'a> {
tooltip_manager: &'a mut TooltipManager, tooltip_manager: &'a mut TooltipManager,
item_imgs: &'a ItemImgs, item_imgs: &'a ItemImgs,
inventory: &'a Inventory, inventory: &'a Inventory,
msm: &'a MaterialStatManifest,
) -> Self { ) -> Self {
Self { Self {
client, client,
@ -94,6 +96,7 @@ impl<'a> Crafting<'a> {
tooltip_manager, tooltip_manager,
item_imgs, item_imgs,
inventory, inventory,
msm,
common: widget::CommonBuilder::default(), common: widget::CommonBuilder::default(),
} }
} }
@ -297,7 +300,7 @@ impl<'a> Widget for Crafting<'a> {
{ {
let output_text = format!("x{}", &recipe.output.1.to_string()); let output_text = format!("x{}", &recipe.output.1.to_string());
// Output Image // Output Image
let (title, desc) = super::util::item_text(&*recipe.output.0); let (title, desc) = super::util::item_text(&*recipe.output.0, self.msm);
let quality_col = get_quality_col(&*recipe.output.0); let quality_col = get_quality_col(&*recipe.output.0);
Button::image(animate_by_pulse( Button::image(animate_by_pulse(
&self &self
@ -488,7 +491,7 @@ impl<'a> Widget for Crafting<'a> {
}; };
frame.set(state.ids.ingredient_frame[i], ui); frame.set(state.ids.ingredient_frame[i], ui);
//Item Image //Item Image
let (title, desc) = super::util::item_text(&*item_def); let (title, desc) = super::util::item_text(&*item_def, self.msm);
Button::image(animate_by_pulse( Button::image(animate_by_pulse(
&self.item_imgs.img_ids_or_not_found_img((&*item_def).into()), &self.item_imgs.img_ids_or_not_found_img((&*item_def).into()),
self.pulse, self.pulse,

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)
{ {
@ -2136,6 +2143,7 @@ impl Hud {
tooltip_manager, tooltip_manager,
&mut self.slot_manager, &mut self.slot_manager,
i18n, i18n,
&msm,
self.pulse, self.pulse,
) )
.set(self.ids.trade, ui_widgets) .set(self.ids.trade, ui_widgets)
@ -2200,6 +2208,7 @@ impl Hud {
tooltip_manager, tooltip_manager,
&self.item_imgs, &self.item_imgs,
&inventory, &inventory,
&msm,
) )
.set(self.ids.crafting_window, ui_widgets) .set(self.ids.crafting_window, 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,
} }
} }
} }
@ -397,7 +400,13 @@ impl<'a> Widget for Skillbar<'a> {
.set(state.ids.stamina_txt, ui); .set(state.ids.stamina_txt, ui);
} }
// Slots // Slots
let content_source = (self.hotbar, self.inventory, self.energy, self.ability_map); // TODO: avoid this let content_source = (
self.hotbar,
self.inventory,
self.energy,
self.ability_map,
self.msm,
); // TODO: avoid this
let image_source = (self.item_imgs, self.imgs); let image_source = (self.item_imgs, self.imgs);
let mut slot_maker = SlotMaker { let mut slot_maker = SlotMaker {
// TODO: is a separate image needed for the frame? // TODO: is a separate image needed for the frame?
@ -623,7 +632,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

@ -7,7 +7,7 @@ use crate::ui::slot::{self, SlotKey, SumSlot};
use common::comp::{ use common::comp::{
item::{ item::{
tool::{AbilityMap, Hands, ToolKind}, tool::{AbilityMap, Hands, ToolKind},
ItemKind, ItemKind, MaterialStatManifest,
}, },
slot::InvSlotId, slot::InvSlotId,
Energy, Inventory, Energy, Inventory,
@ -101,7 +101,13 @@ pub enum HotbarImage {
BowJumpBurst, BowJumpBurst,
} }
type HotbarSource<'a> = (&'a hotbar::State, &'a Inventory, &'a Energy, &'a AbilityMap); type HotbarSource<'a> = (
&'a hotbar::State,
&'a Inventory,
&'a Energy,
&'a AbilityMap,
&'a MaterialStatManifest,
);
type HotbarImageSource<'a> = (&'a ItemImgs, &'a img_ids::Imgs); type HotbarImageSource<'a> = (&'a ItemImgs, &'a img_ids::Imgs);
impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot { impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
@ -109,7 +115,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
fn image_key( fn image_key(
&self, &self,
(hotbar, inventory, energy, ability_map): &HotbarSource<'a>, (hotbar, inventory, energy, ability_map, msm): &HotbarSource<'a>,
) -> Option<(Self::ImageKey, Option<Color>)> { ) -> Option<(Self::ImageKey, Option<Color>)> {
hotbar.get(*self).and_then(|contents| match contents { hotbar.get(*self).and_then(|contents| match contents {
hotbar::SlotContents::Inventory(idx) => inventory hotbar::SlotContents::Inventory(idx) => inventory
@ -130,7 +136,7 @@ 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(&msm, item.components(), ability_map)
.abilities .abilities
.get(0) .get(0)
{ {
@ -173,7 +179,7 @@ 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(&msm, item.components(), ability_map)
.abilities .abilities
.get(skill_index) .get(skill_index)
{ {
@ -192,7 +198,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
}) })
} }
fn amount(&self, (hotbar, inventory, _, _): &HotbarSource<'a>) -> Option<u32> { fn amount(&self, (hotbar, inventory, _, _, _): &HotbarSource<'a>) -> Option<u32> {
hotbar hotbar
.get(*self) .get(*self)
.and_then(|content| match content { .and_then(|content| match content {

View File

@ -15,7 +15,10 @@ use crate::{
}; };
use client::Client; use client::Client;
use common::{ use common::{
comp::{inventory::item::Quality, Inventory}, comp::{
inventory::item::{MaterialStatManifest, Quality},
Inventory,
},
trade::{PendingTrade, TradeAction, TradePhase}, trade::{PendingTrade, TradeAction, TradePhase},
}; };
use common_net::sync::WorldSyncExt; use common_net::sync::WorldSyncExt;
@ -61,6 +64,7 @@ pub struct Trade<'a> {
common: widget::CommonBuilder, common: widget::CommonBuilder,
slot_manager: &'a mut SlotManager, slot_manager: &'a mut SlotManager,
localized_strings: &'a Localization, localized_strings: &'a Localization,
msm: &'a MaterialStatManifest,
pulse: f32, pulse: f32,
} }
@ -74,6 +78,7 @@ impl<'a> Trade<'a> {
tooltip_manager: &'a mut TooltipManager, tooltip_manager: &'a mut TooltipManager,
slot_manager: &'a mut SlotManager, slot_manager: &'a mut SlotManager,
localized_strings: &'a Localization, localized_strings: &'a Localization,
msm: &'a MaterialStatManifest,
pulse: f32, pulse: f32,
) -> Self { ) -> Self {
Self { Self {
@ -84,9 +89,9 @@ impl<'a> Trade<'a> {
rot_imgs, rot_imgs,
tooltip_manager, tooltip_manager,
common: widget::CommonBuilder::default(), common: widget::CommonBuilder::default(),
//tooltip_manager,
slot_manager, slot_manager,
localized_strings, localized_strings,
msm,
pulse, pulse,
} }
} }
@ -295,7 +300,7 @@ impl<'a> Trade<'a> {
); );
let slot_id = state.ids.inv_slots[i + who * MAX_TRADE_SLOTS]; let slot_id = state.ids.inv_slots[i + who * MAX_TRADE_SLOTS];
if let Some(Some(item)) = slot.invslot.and_then(|slotid| inventory.slot(slotid)) { if let Some(Some(item)) = slot.invslot.and_then(|slotid| inventory.slot(slotid)) {
let (title, desc) = super::util::item_text(item); let (title, desc) = super::util::item_text(item, self.msm);
let quality_col = get_quality_col(item); let quality_col = get_quality_col(item);
let quality_col_img = match item.quality() { let quality_col_img = match item.quality() {
Quality::Low => self.imgs.inv_slot_grey, Quality::Low => self.imgs.inv_slot_grey,

View File

@ -1,35 +1,53 @@
use common::comp::item::{ use common::comp::item::{
armor::{Armor, ArmorKind, Protection}, armor::{Armor, ArmorKind, Protection},
tool::{Hands, Tool, ToolKind}, tool::{Hands, StatKind, Stats, Tool, ToolKind},
Item, ItemDesc, ItemKind, ModularComponent, Item, ItemDesc, ItemKind, MaterialStatManifest, ModularComponent,
}; };
use std::{borrow::Cow, fmt::Write}; use std::{borrow::Cow, fmt::Write};
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),
msm: &'a MaterialStatManifest,
) -> (&'a str, Cow<'a, str>) { ) -> (&'a str, Cow<'a, str>) {
item.map_or_else( item.map_or_else(
|| { || {
let (title, desc) = empty(); let (title, desc) = empty();
(title, Cow::Borrowed(desc)) (title, Cow::Borrowed(desc))
}, },
item_text, |item| item_text(item, msm),
) )
} }
pub fn item_text<'a>(item: &'a impl ItemDesc) -> (&'_ str, Cow<'a, str>) { pub fn item_text<'a>(
item: &'a impl ItemDesc,
msm: &'a MaterialStatManifest,
) -> (&'a str, Cow<'a, str>) {
let desc: Cow<str> = match item.kind() { let desc: Cow<str> = match item.kind() {
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(
ItemKind::ModularComponent(mc) => Cow::Owned(modular_component_desc(mc)), &tool,
item.components(),
&msm,
item.description(),
)),
ItemKind::ModularComponent(mc) => Cow::Owned(modular_component_desc(
mc,
item.components(),
&msm,
item.description(),
)),
ItemKind::Glider(_glider) => Cow::Owned(glider_desc(item.description())), ItemKind::Glider(_glider) => Cow::Owned(glider_desc(item.description())),
ItemKind::Consumable { .. } => Cow::Owned(consumable_desc(item.description())), ItemKind::Consumable { .. } => Cow::Owned(consumable_desc(item.description())),
ItemKind::Throwable { .. } => Cow::Owned(throwable_desc(item.description())), ItemKind::Throwable { .. } => Cow::Owned(throwable_desc(item.description())),
ItemKind::Utility { .. } => Cow::Owned(utility_desc(item.description())), ItemKind::Utility { .. } => Cow::Owned(utility_desc(item.description())),
ItemKind::Ingredient { .. } => Cow::Owned(ingredient_desc(item.description())), ItemKind::Ingredient { .. } => Cow::Owned(ingredient_desc(
item.description(),
item.item_definition_id(),
msm,
)),
ItemKind::Lantern { .. } => Cow::Owned(lantern_desc(item.description())), ItemKind::Lantern { .. } => Cow::Owned(lantern_desc(item.description())),
ItemKind::TagExamples { .. } => Cow::Borrowed(item.description()), ItemKind::TagExamples { .. } => Cow::Borrowed(item.description()),
//_ => Cow::Borrowed(item.description()), //_ => Cow::Borrowed(item.description()),
@ -39,8 +57,24 @@ pub fn item_text<'a>(item: &'a impl ItemDesc) -> (&'_ str, Cow<'a, str>) {
} }
// TODO: localization // TODO: localization
fn modular_component_desc(mc: &ModularComponent) -> String { fn modular_component_desc(
format!("Modular Component\n\n{:?}", mc) mc: &ModularComponent,
components: &[Item],
msm: &MaterialStatManifest,
description: &str,
) -> String {
let stats = StatKind::Direct(mc.stats).resolve_stats(msm, components);
let statblock = statblock_desc(&stats);
let mut result = format!("Modular Component\n\n{}\n\n{}", statblock, description);
if !components.is_empty() {
result += "\n\nMade from:\n";
for component in components {
result += component.name();
result += "\n"
}
result += "\n";
}
result
} }
fn glider_desc(desc: &str) -> String { format!("Glider\n\n{}\n\n<Right-Click to use>", desc) } fn glider_desc(desc: &str) -> String { format!("Glider\n\n{}\n\n<Right-Click to use>", desc) }
@ -54,7 +88,14 @@ fn throwable_desc(desc: &str) -> String {
fn utility_desc(desc: &str) -> String { format!("{}\n\n<Right-Click to use>", desc) } fn utility_desc(desc: &str) -> String { format!("{}\n\n<Right-Click to use>", desc) }
fn ingredient_desc(desc: &str) -> String { format!("Crafting Ingredient\n\n{}", desc) } fn ingredient_desc(desc: &str, item_id: &str, msm: &MaterialStatManifest) -> String {
let mut result = format!("Crafting Ingredient\n\n{}", desc);
if let Some(stats) = msm.0.get(item_id) {
result += "\n\nStat multipliers:\n";
result += &statblock_desc(stats);
}
result
}
fn lantern_desc(desc: &str) -> String { format!("Lantern\n\n{}\n\n<Right-Click to use>", desc) } fn lantern_desc(desc: &str) -> String { format!("Lantern\n\n{}\n\n<Right-Click to use>", desc) }
@ -102,7 +143,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",
@ -119,26 +160,16 @@ fn tool_desc(tool: &Tool, components: &[Item], desc: &str) -> String {
}; };
// Get tool stats // Get tool stats
let power = tool.base_power(components); let stats = tool.stats.resolve_stats(msm, components).clamp_speed();
//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 mut result = format!( let mut result = format!("{}-Handed {}\n\n", hands, kind);
"{}-Handed {}\n\nDPS: {:0.1}\n\nPower: {:0.1}\n\nSpeed: {:0.1}\n\n", result += &statblock_desc(&stats);
// add back when ready for poise
//"{}\n\nDPS: {:0.1}\n\nPower: {:0.1}\n\nPoise Strength: {:0.1}\n\nSpeed: \
// {:0.1}\n\n{}\n\n<Right-Click to use>",
hands,
kind,
speed * power * 10.0, // Damage per second
power * 10.0,
//poise_strength * 10.0,
speed
);
if !components.is_empty() { if !components.is_empty() {
result += "Made from:\n"; result += "Made from:\n";
for component in components { for component in components {
@ -154,6 +185,19 @@ fn tool_desc(tool: &Tool, components: &[Item], desc: &str) -> String {
result result
} }
fn statblock_desc(stats: &Stats) -> String {
format!(
"DPS: {:0.1}\n\nPower: {:0.1}\n\nSpeed: {:0.1}\n\n",
// add back when ready for poise
//"{}\n\nDPS: {:0.1}\n\nPower: {:0.1}\n\nPoise Strength: {:0.1}\n\nSpeed: \
// {:0.1}\n\n{}\n\n<Right-Click to use>",
stats.speed * stats.power * 10.0, // Damage per second
stats.power * 10.0,
//stats.poise_strength * 10.0,
stats.speed
)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -200,11 +244,29 @@ mod tests {
#[test] #[test]
fn test_ingredient_desc() { fn test_ingredient_desc() {
let item_description = "mushrooms"; let mut testmsm = MaterialStatManifest(hashbrown::HashMap::new());
testmsm.0.insert(
"common.items.crafting_ing.bronze_ingot".to_string(),
Stats {
equip_time_millis: 0,
power: 3.0,
poise_strength: 5.0,
speed: 7.0,
},
);
assert_eq!( assert_eq!(
"Crafting Ingredient\n\nmushrooms", "Crafting Ingredient\n\nmushrooms",
ingredient_desc(item_description) ingredient_desc("mushrooms", "common.items.food.mushroom", &testmsm)
);
assert_eq!(
"Crafting Ingredient\n\nA bronze ingot.\n\nStat multipliers:\nDPS: 210.0\n\nPower: \
30.0\n\nSpeed: 7.0\n\n",
ingredient_desc(
"A bronze ingot.",
"common.items.crafting_ing.bronze_ingot",
&testmsm
)
); );
} }