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",
),
quality: Common,
tags: [],
)
tags: [MetalIngot],
)

View File

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

View File

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

View File

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

View File

@ -5,5 +5,5 @@ ItemDef(
kind: "IronIngot",
),
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",
),
quality: Common,
tags: [],
)
tags: [MetalIngot],
)

View File

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

View File

@ -18,6 +18,5 @@ ItemDef(
],
),
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),
(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,
world_map,
recipe_book,
material_stats,
ability_map,
} => {
// Initialize `State`
@ -264,6 +265,7 @@ impl Client {
let entity = state.ecs_mut().apply_entity_package(entity_package);
*state.ecs_mut().write_resource() = time_of_day;
*state.ecs_mut().write_resource() = material_stats;
state.ecs_mut().insert(ability_map);
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 common::{
character::{self, CharacterItem},
comp::{self, invite::InviteKind},
comp::{self, invite::InviteKind, item::MaterialStatManifest},
outcome::Outcome,
recipe::RecipeBook,
resources::TimeOfDay,
@ -56,6 +56,7 @@ pub enum ServerInit {
client_timeout: Duration,
world_map: crate::msg::world_msg::WorldMapMsg,
recipe_book: RecipeBook,
material_stats: MaterialStatManifest,
ability_map: comp::item::tool::AbilityMap,
},
}

View File

@ -7,7 +7,7 @@ use crate::{
item::{
armor::Protection,
tool::{Tool, ToolKind},
Item, ItemKind,
Item, ItemKind, MaterialStatManifest,
},
slot::EquipSlot,
},
@ -654,30 +654,36 @@ pub fn get_weapons(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
}
#[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 =
equipped_item_and_tool(inv, EquipSlot::Mainhand).map_or(0.0, |(item, tool)| {
tool.base_power(item.components())
* tool.base_speed(item.components())
tool.base_power(msm, item.components())
* tool.base_speed(msm, item.components())
* (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32)
});
let second_damage =
equipped_item_and_tool(inv, EquipSlot::Offhand).map_or(0.0, |(item, tool)| {
tool.base_power(item.components())
* tool.base_speed(item.components())
tool.base_power(msm, item.components())
* tool.base_speed(msm, item.components())
* (1.0 + 0.05 * skillset.earned_sp(SkillGroupKind::Weapon(tool.kind)) as f32)
});
active_damage.max(second_damage)
}
#[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 offensive_weighting = 1.0;
let defensive_rating = health.maximum() as f32
/ (1.0 - Damage::compute_damage_reduction(inventory)).max(0.00001)
/ 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;
let combined_rating = (offensive_rating * offensive_weighting
+ defensive_rating * defensive_weighting)

View File

@ -4,7 +4,7 @@ pub mod tool;
// Reexports
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::{
assets::{self, AssetExt, Error},
@ -87,10 +87,11 @@ pub trait TagExampleInfo {
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 {
ClothItem,
ModularComponent(ModularComponentTag),
MetalIngot,
}
impl TagExampleInfo for ItemTag {
@ -98,6 +99,7 @@ impl TagExampleInfo for ItemTag {
match self {
ItemTag::ClothItem => "cloth item",
ItemTag::ModularComponent(kind) => kind.name(),
ItemTag::MetalIngot => "metal ingot",
}
}
@ -105,6 +107,7 @@ impl TagExampleInfo for ItemTag {
match self {
ItemTag::ClothItem => "common.items.tag_examples.cloth_item",
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>,
}
impl From<(&ItemKind, &[Item], &AbilityMap)> for ItemConfig {
fn from((item_kind, components, map): (&ItemKind, &[Item], &AbilityMap)) -> Self {
impl From<(&ItemKind, &[Item], &AbilityMap, &MaterialStatManifest)> for ItemConfig {
fn from(
(item_kind, components, map, msm): (&ItemKind, &[Item], &AbilityMap, &MaterialStatManifest),
) -> Self {
if let ItemKind::Tool(tool) = item_kind {
let abilities = tool.get_abilities(components, map);
let abilities = tool.get_abilities(msm, components, map);
return ItemConfig {
abilities,
@ -245,7 +250,7 @@ impl ItemDef {
ItemKind::Tool(tool::Tool {
stats: tool::StatKind::Modular,
..
})
}) | ItemKind::ModularComponent(_)
)
}
@ -372,12 +377,16 @@ impl Item {
// loadout when no weapon is present
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();
if inner_item.is_modular() {
// recipe ensures that types match (i.e. no axe heads on a sword hilt, or double
// 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 {
@ -388,7 +397,7 @@ impl Item {
item_def: inner_item,
item_config: None,
};
item.update_item_config();
item.update_item_config(msm);
item
}
@ -396,7 +405,8 @@ impl Item {
/// Panics if the asset does not exist.
pub fn new_from_asset_expect(asset_specifier: &str) -> Self {
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
@ -409,12 +419,13 @@ impl Item {
/// it exists
pub fn new_from_asset(asset: &str) -> Result<Self, Error> {
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
pub fn duplicate(&self) -> Self {
Item::new_from_item_def(Arc::clone(&self.item_def), &self.components)
pub fn duplicate(&self, msm: &MaterialStatManifest) -> Self {
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
@ -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
// persistence, but will definitely be needed once enhancement slots are
// added to prevent putting a sword into another sword)
self.components.push(component);
// 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() {
Some(Box::new(ItemConfig::from((
self.kind(),
self.components(),
&self.item_def.ability_map,
msm,
))))
} else {
None
@ -644,6 +656,7 @@ pub trait ItemDesc {
fn num_slots(&self) -> u16;
fn item_definition_id(&self) -> &str;
fn components(&self) -> &[Item];
fn tags(&self) -> &[ItemTag];
}
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 components(&self) -> &[Item] { &self.components }
fn tags(&self) -> &[ItemTag] { &self.item_def.tags }
}
impl ItemDesc for ItemDef {
@ -676,6 +691,8 @@ impl ItemDesc for ItemDef {
fn item_definition_id(&self) -> &str { &self.item_definition_id }
fn components(&self) -> &[Item] { &[] }
fn tags(&self) -> &[ItemTag] { &self.tags }
}
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
use crate::{
assets::{self, Asset},
assets::{self, Asset, AssetExt},
comp::{item::ItemKind, skills::Skill, CharacterAbility, Item},
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use std::{
ops::{AddAssign, DivAssign, MulAssign},
time::Duration,
};
use tracing::error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -69,6 +72,67 @@ impl Stats {
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)]
@ -78,30 +142,44 @@ pub enum 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 {
StatKind::Direct(stats) => *stats,
StatKind::Modular => Stats::zeroed(),
};
let mut multipliers: Vec<Stats> = Vec::new();
for item in components.iter() {
if let ItemKind::ModularComponent(mc) = item.kind() {
stats.equip_time_millis += mc.stats.equip_time_millis;
stats.power += mc.stats.power;
stats.poise_strength += mc.stats.poise_strength;
stats.speed += mc.stats.speed;
match item.kind() {
ItemKind::ModularComponent(mc) => {
let inner_stats =
StatKind::Direct(mc.stats).resolve_stats(msm, item.components());
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
// enforce speed >= 0.1
stats.speed = stats.speed.max(0.1);
// Take the average of the material multipliers, to allow alloyed blades
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;
}
stats
}
}
impl From<(&[Item], &Tool)> for Stats {
fn from((components, tool): (&[Item], &Tool)) -> Self {
let raw_stats = tool.stats.resolve_stats(components);
impl From<(&MaterialStatManifest, &[Item], &Tool)> for Stats {
fn from((msm, components, tool): (&MaterialStatManifest, &[Item], &Tool)) -> Self {
let raw_stats = tool.stats.resolve_stats(msm, components).clamp_speed();
let (power, speed) = match tool.hands {
Hands::One => (0.67, 1.33),
// TODO: Restore this when one-handed weapons are made accessible
@ -162,29 +240,33 @@ impl Tool {
}
// Keep power between 0.5 and 2.00
pub fn base_power(&self, components: &[Item]) -> f32 {
self.stats.resolve_stats(components).power
pub fn base_power(&self, msm: &MaterialStatManifest, components: &[Item]) -> f32 {
self.stats.resolve_stats(msm, components).power
}
pub fn base_poise_strength(&self, components: &[Item]) -> f32 {
self.stats.resolve_stats(components).poise_strength
pub fn base_poise_strength(&self, msm: &MaterialStatManifest, components: &[Item]) -> f32 {
self.stats.resolve_stats(msm, components).poise_strength
}
pub fn base_speed(&self, components: &[Item]) -> f32 {
self.stats.resolve_stats(components).speed
pub fn base_speed(&self, msm: &MaterialStatManifest, components: &[Item]) -> f32 {
self.stats
.resolve_stats(msm, components)
.clamp_speed()
.speed
}
pub fn equip_time(&self, components: &[Item]) -> Duration {
Duration::from_millis(self.stats.resolve_stats(components).equip_time_millis as u64)
pub fn equip_time(&self, msm: &MaterialStatManifest, components: &[Item]) -> Duration {
Duration::from_millis(self.stats.resolve_stats(msm, components).equip_time_millis as u64)
}
pub fn get_abilities(
&self,
msm: &MaterialStatManifest,
components: &[Item],
map: &AbilityMap,
) -> AbilitySet<CharacterAbility> {
if let Some(set) = map.0.get(&self.kind).cloned() {
set.modified_by_tool(&self, components)
set.modified_by_tool(&self, msm, components)
} else {
error!(
"ToolKind: {:?} has no AbilitySet in the ability map falling back to default",
@ -203,8 +285,13 @@ pub struct AbilitySet<T> {
}
impl AbilitySet<CharacterAbility> {
pub fn modified_by_tool(self, tool: &Tool, components: &[Item]) -> Self {
let stats = Stats::from((components, tool));
pub fn modified_by_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))
}
}

View File

@ -9,7 +9,7 @@ use tracing::{debug, trace, warn};
use crate::{
comp::{
inventory::{
item::ItemDef,
item::{ItemDef, MaterialStatManifest},
loadout::Loadout,
slot::{EquipSlot, Slot, SlotError},
},
@ -257,9 +257,9 @@ impl Inventory {
}
/// 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) {
let mut return_item = item.duplicate();
let mut return_item = item.duplicate(msm);
if item.is_stackable() && item.amount() > 1 {
item.decrease_amount(1).ok()?;

View File

@ -3,7 +3,7 @@ use crate::comp::{
armor,
armor::{ArmorKind, Protection},
tool::AbilityMap,
ItemDef, ItemKind, Quality,
ItemDef, ItemKind, MaterialStatManifest, Quality,
},
Item,
};
@ -23,5 +23,5 @@ pub(super) fn get_test_bag(slots: u16) -> Item {
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::{
assets::{self, AssetExt, AssetHandle},
comp::{
item::{modular, ItemDef, ItemTag},
item::{modular, ItemDef, ItemTag, MaterialStatManifest},
Inventory, Item,
},
};
@ -27,6 +27,7 @@ impl Recipe {
pub fn perform(
&self,
inv: &mut Inventory,
msm: &MaterialStatManifest,
) -> Result<Option<(Item, u32)>, Vec<(&RecipeInput, u32)>> {
// Get ingredient cells from inventory,
let mut components = Vec::new();
@ -35,13 +36,16 @@ impl Recipe {
.into_iter()
.for_each(|(pos, n)| {
(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);
})
});
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) {
return Ok(Some((item, self.output.1 - i)));
}

View File

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

View File

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

View File

@ -5,7 +5,10 @@ use specs::{
use common::{
comp::{
inventory::slot::{EquipSlot, Slot},
inventory::{
item::MaterialStatManifest,
slot::{EquipSlot, Slot},
},
Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Melee, Mounting, Ori,
PhysicsState, Poise, PoiseState, Pos, StateUpdate, Stats, Vel,
},
@ -62,6 +65,7 @@ pub struct ReadData<'a> {
uids: ReadStorage<'a, Uid>,
mountings: ReadStorage<'a, Mounting>,
stats: ReadStorage<'a, Stats>,
msm: Read<'a, MaterialStatManifest>,
}
/// ## Character Behavior System
@ -252,7 +256,12 @@ impl<'a> System<'a> for Sys {
};
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 {
CharacterState::Idle => states::idle::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);
}
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 {
CharacterState::Idle => states::idle::Data.behavior(&j),

View File

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

View File

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

View File

@ -13,6 +13,7 @@ use common::{
comp::{
self, aura, buff,
chat::{KillSource, KillType},
inventory::item::MaterialStatManifest,
object, Alignment, Body, CharacterState, Energy, EnergyChange, Group, Health, HealthChange,
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;
// TODO: Scale xp from skillset rather than health, when NPCs have their own
// skillsets
let mut exp_reward =
combat::combat_rating(entity_inventory, entity_health, entity_stats, *entity_body)
* 2.5;
let msm = state.ecs().read_resource::<MaterialStatManifest>();
let mut exp_reward = combat::combat_rating(
entity_inventory,
entity_health,
entity_stats,
*entity_body,
&msm,
) * 2.5;
// Distribute EXP to group
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)
} 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() {
ItemKind::Consumable { kind, effect, .. } => {
maybe_effect = Some(effect.clone());
@ -484,9 +487,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo
.get_mut(entity)
{
let recipe_book = default_recipe_book().read();
let craft_result = recipe_book
.get(&recipe)
.and_then(|r| r.perform(&mut inv).ok());
let craft_result = recipe_book.get(&recipe).and_then(|r| {
r.perform(
&mut inv,
&state.ecs().read_resource::<item::MaterialStatManifest>(),
)
.ok()
});
// FIXME: We should really require the drop and write to be atomic!
if craft_result.is_some() {

View File

@ -1,6 +1,6 @@
use crate::Server;
use common::{
comp::inventory::Inventory,
comp::inventory::{item::MaterialStatManifest, Inventory},
trade::{PendingTrade, TradeAction, TradeId, TradeResult, Trades},
};
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 msm = ecs.read_resource::<MaterialStatManifest>();
for who in [0, 1].iter().cloned() {
for (slot, quantity) in trade.offers[who].iter() {
// 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
.get_mut(entities[who])
.expect(invmsg)
.take(*slot)
.take(*slot, &msm)
.map(|item| items[who].push(item));
}
}

View File

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

View File

@ -9,7 +9,7 @@ extern crate diesel;
use super::{error::Error, models::*, schema, VelorenTransaction};
use crate::{
comp,
comp::Inventory,
comp::{item::MaterialStatManifest, Inventory},
persistence::{
character::conversions::{
convert_body_from_database, convert_body_to_database_json,
@ -78,6 +78,7 @@ pub fn load_character_data(
requesting_player_uuid: String,
char_id: CharacterId,
connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterDataResult {
use schema::{body::dsl::*, character::dsl::*, skill_group::dsl::*};
@ -127,6 +128,7 @@ pub fn load_character_data(
&inventory_items,
character_containers.loadout_container_id,
&loadout_items,
msm,
)?,
char_waypoint,
))
@ -142,6 +144,7 @@ pub fn load_character_data(
pub fn load_character_list(
player_uuid_: &str,
connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterListResult {
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 =
convert_loadout_from_database_items(loadout_container_id, &loadout_items)?;
convert_loadout_from_database_items(loadout_container_id, &loadout_items, msm)?;
Ok(CharacterItem {
character: char,
@ -186,6 +189,7 @@ pub fn create_character(
character_alias: &str,
persisted_components: PersistedComponents,
connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterCreationResult {
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.
@ -325,6 +329,7 @@ pub fn delete_character(
requesting_player_uuid: &str,
char_id: CharacterId,
connection: VelorenTransaction,
msm: &MaterialStatManifest,
) -> CharacterListResult {
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

View File

@ -11,6 +11,7 @@ use common::{
character::CharacterId,
comp::{
inventory::{
item::MaterialStatManifest,
loadout::{Loadout, LoadoutError},
loadout_builder::LoadoutBuilder,
slot::InvSlotId,
@ -225,6 +226,7 @@ pub fn convert_inventory_from_database_items(
inventory_items: &[Item],
loadout_container_id: i64,
loadout_items: &[Item],
msm: &MaterialStatManifest,
) -> Result<Inventory, Error> {
// Loadout items must be loaded before inventory items since loadout items
// 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
// 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 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) {
if let Some(Some(parent)) = inventory.slot_mut(slot(&inventory_items[j].position)?) {
parent.add_component(item);
parent.add_component(item, msm);
} else {
return Err(Error::ConversionError(format!(
"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(
loadout_container_id: i64,
database_items: &[Item],
msm: &MaterialStatManifest,
) -> Result<Loadout, Error> {
let loadout_builder = LoadoutBuilder::new();
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) {
loadout
.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)?;
} else {

View File

@ -3,8 +3,12 @@ use crate::persistence::{
error::Error,
establish_connection, PersistedComponents,
};
use common::character::{CharacterId, CharacterItem};
use common::{
character::{CharacterId, CharacterItem},
comp::inventory::item::MaterialStatManifest,
};
use crossbeam_channel::{self, TryIter};
use lazy_static::lazy_static;
use std::path::Path;
use tracing::error;
@ -66,6 +70,13 @@ pub struct CharacterLoader {
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 {
pub fn new(db_dir: &Path) -> diesel::QueryResult<Self> {
let (update_tx, internal_rx) = crossbeam_channel::unbounded::<CharacterLoaderRequest>();
@ -93,6 +104,7 @@ impl CharacterLoader {
&character_alias,
persisted_components,
txn,
&MATERIAL_STATS_MANIFEST,
)
},
)),
@ -100,19 +112,37 @@ impl CharacterLoader {
player_uuid,
character_id,
} => 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 } => {
CharacterLoaderResponseKind::CharacterList(
conn.transaction(|txn| load_character_list(&player_uuid, txn)),
)
CharacterLoaderResponseKind::CharacterList(conn.transaction(
|txn| {
load_character_list(
&player_uuid,
txn,
&MATERIAL_STATS_MANIFEST,
)
},
))
},
CharacterLoaderRequestKind::LoadCharacterData {
player_uuid,
character_id,
} => {
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() {
error!(

View File

@ -18,7 +18,10 @@ use crate::{
use client::Client;
use common::{
combat::{combat_rating, Damage},
comp::{item::Quality, Body, Energy, Health, Stats},
comp::{
item::{MaterialStatManifest, Quality},
Body, Energy, Health, Stats,
},
};
use conrod_core::{
color,
@ -101,6 +104,7 @@ pub struct Bag<'a> {
energy: &'a Energy,
show: &'a Show,
body: &'a Body,
msm: &'a MaterialStatManifest,
}
impl<'a> Bag<'a> {
@ -120,6 +124,7 @@ impl<'a> Bag<'a> {
energy: &'a Energy,
show: &'a Show,
body: &'a Body,
msm: &'a MaterialStatManifest,
) -> Self {
Self {
client,
@ -137,6 +142,7 @@ impl<'a> Bag<'a> {
health,
show,
body,
msm,
}
}
}
@ -433,7 +439,7 @@ impl<'a> Widget for Bag<'a> {
});
// Stats
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);
for i in STATS.iter().copied().enumerate() {
let btn = Button::image(match i.1 {
@ -503,6 +509,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Head)),
|| (i18n.get("hud.bag.head"), ""),
&self.msm,
);
let head_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Head))
@ -525,6 +532,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Neck)),
|| (i18n.get("hud.bag.neck"), ""),
&self.msm,
);
let neck_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Neck))
@ -548,6 +556,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Chest)),
|| (i18n.get("hud.bag.chest"), ""),
&self.msm,
);
let chest_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Chest))
@ -570,6 +579,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Shoulders)),
|| (i18n.get("hud.bag.shoulders"), ""),
&self.msm,
);
let shoulder_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Shoulders))
@ -592,6 +602,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Hands)),
|| (i18n.get("hud.bag.hands"), ""),
&self.msm,
);
let chest_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Hands))
@ -614,6 +625,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Belt)),
|| (i18n.get("hud.bag.belt"), ""),
&self.msm,
);
let belt_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Belt))
@ -636,6 +648,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Legs)),
|| (i18n.get("hud.bag.legs"), ""),
&self.msm,
);
let legs_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Legs))
@ -658,6 +671,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring1)),
|| (i18n.get("hud.bag.ring"), ""),
&self.msm,
);
let ring_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Ring1))
@ -680,6 +694,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Ring2)),
|| (i18n.get("hud.bag.ring"), ""),
&self.msm,
);
let ring2_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Ring2))
@ -702,6 +717,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Back)),
|| (i18n.get("hud.bag.back"), ""),
&self.msm,
);
let back_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Back))
@ -724,6 +740,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Feet)),
|| (i18n.get("hud.bag.feet"), ""),
&self.msm,
);
let foot_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Feet))
@ -743,9 +760,11 @@ impl<'a> Widget for Bag<'a> {
)
.set(state.ids.feet_slot, ui);
// Lantern
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Lantern), || {
(i18n.get("hud.bag.lantern"), "")
});
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Lantern),
|| (i18n.get("hud.bag.lantern"), ""),
&self.msm,
);
let lantern_q_col = inventory
.equipped(EquipSlot::Lantern)
.map(|item| get_quality_col(item))
@ -764,9 +783,11 @@ impl<'a> Widget for Bag<'a> {
)
.set(state.ids.lantern_slot, ui);
// Glider
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Glider), || {
(i18n.get("hud.bag.glider"), "")
});
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Glider),
|| (i18n.get("hud.bag.glider"), ""),
&self.msm,
);
let glider_q_col = inventory
.equipped(EquipSlot::Glider)
.map(|item| get_quality_col(item))
@ -788,6 +809,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Tabard)),
|| (i18n.get("hud.bag.tabard"), ""),
&self.msm,
);
let tabard_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Tabard))
@ -807,9 +829,11 @@ impl<'a> Widget for Bag<'a> {
)
.set(state.ids.tabard_slot, ui);
// Mainhand/Left-Slot
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Mainhand), || {
(i18n.get("hud.bag.mainhand"), "")
});
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Mainhand),
|| (i18n.get("hud.bag.mainhand"), ""),
&self.msm,
);
let mainhand_q_col = inventory
.equipped(EquipSlot::Mainhand)
.map(|item| get_quality_col(item))
@ -828,9 +852,11 @@ impl<'a> Widget for Bag<'a> {
)
.set(state.ids.mainhand_slot, ui);
// Offhand/Right-Slot
let (title, desc) = loadout_slot_text(inventory.equipped(EquipSlot::Offhand), || {
(i18n.get("hud.bag.offhand"), "")
});
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Offhand),
|| (i18n.get("hud.bag.offhand"), ""),
&self.msm,
);
let offhand_q_col = inventory
.equipped(EquipSlot::Offhand)
.map(|item| get_quality_col(item))
@ -853,6 +879,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag1)),
|| (i18n.get("hud.bag.bag"), ""),
&self.msm,
);
let bag1_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag1))
@ -879,6 +906,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag2)),
|| (i18n.get("hud.bag.bag"), ""),
&self.msm,
);
let bag2_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag2))
@ -901,6 +929,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag3)),
|| (i18n.get("hud.bag.bag"), ""),
&self.msm,
);
let bag3_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag3))
@ -923,6 +952,7 @@ impl<'a> Widget for Bag<'a> {
let (title, desc) = loadout_slot_text(
inventory.equipped(EquipSlot::Armor(ArmorSlot::Bag4)),
|| (i18n.get("hud.bag.bag"), ""),
&self.msm,
);
let bag4_q_col = inventory
.equipped(EquipSlot::Armor(ArmorSlot::Bag4))
@ -1005,7 +1035,7 @@ impl<'a> Widget for Bag<'a> {
}
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_img = match item.quality() {
Quality::Low => self.imgs.inv_slot_grey,

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ use common::comp::{
inventory::slot::EquipSlot,
item::{
tool::{AbilityMap, Tool, ToolKind},
Hands, Item, ItemKind,
Hands, Item, ItemKind, MaterialStatManifest,
},
Energy, Health, Inventory,
};
@ -140,6 +140,7 @@ pub struct Skillbar<'a> {
#[conrod(common_builder)]
common: widget::CommonBuilder,
ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
}
impl<'a> Skillbar<'a> {
@ -161,6 +162,7 @@ impl<'a> Skillbar<'a> {
slot_manager: &'a mut slots::SlotManager,
localized_strings: &'a Localization,
ability_map: &'a AbilityMap,
msm: &'a MaterialStatManifest,
) -> Self {
Self {
global_state,
@ -180,6 +182,7 @@ impl<'a> Skillbar<'a> {
slot_manager,
localized_strings,
ability_map,
msm,
}
}
}
@ -397,7 +400,13 @@ impl<'a> Widget for Skillbar<'a> {
.set(state.ids.stamina_txt, ui);
}
// 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 mut slot_maker = SlotMaker {
// 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 {
if self.energy.current()
>= tool
.get_abilities(item.components(), self.ability_map)
.get_abilities(&self.msm, item.components(), self.ability_map)
.secondary
.get_energy_cost()
{

View File

@ -7,7 +7,7 @@ use crate::ui::slot::{self, SlotKey, SumSlot};
use common::comp::{
item::{
tool::{AbilityMap, Hands, ToolKind},
ItemKind,
ItemKind, MaterialStatManifest,
},
slot::InvSlotId,
Energy, Inventory,
@ -101,7 +101,13 @@ pub enum HotbarImage {
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);
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(
&self,
(hotbar, inventory, energy, ability_map): &HotbarSource<'a>,
(hotbar, inventory, energy, ability_map, msm): &HotbarSource<'a>,
) -> Option<(Self::ImageKey, Option<Color>)> {
hotbar.get(*self).and_then(|contents| match contents {
hotbar::SlotContents::Inventory(idx) => inventory
@ -130,7 +136,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
(
i,
if let Some(skill) = tool
.get_abilities(item.components(), ability_map)
.get_abilities(&msm, item.components(), ability_map)
.abilities
.get(0)
{
@ -173,7 +179,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
(
i,
if let Some(skill) = tool
.get_abilities(item.components(), ability_map)
.get_abilities(&msm, item.components(), ability_map)
.abilities
.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
.get(*self)
.and_then(|content| match content {

View File

@ -15,7 +15,10 @@ use crate::{
};
use client::Client;
use common::{
comp::{inventory::item::Quality, Inventory},
comp::{
inventory::item::{MaterialStatManifest, Quality},
Inventory,
},
trade::{PendingTrade, TradeAction, TradePhase},
};
use common_net::sync::WorldSyncExt;
@ -61,6 +64,7 @@ pub struct Trade<'a> {
common: widget::CommonBuilder,
slot_manager: &'a mut SlotManager,
localized_strings: &'a Localization,
msm: &'a MaterialStatManifest,
pulse: f32,
}
@ -74,6 +78,7 @@ impl<'a> Trade<'a> {
tooltip_manager: &'a mut TooltipManager,
slot_manager: &'a mut SlotManager,
localized_strings: &'a Localization,
msm: &'a MaterialStatManifest,
pulse: f32,
) -> Self {
Self {
@ -84,9 +89,9 @@ impl<'a> Trade<'a> {
rot_imgs,
tooltip_manager,
common: widget::CommonBuilder::default(),
//tooltip_manager,
slot_manager,
localized_strings,
msm,
pulse,
}
}
@ -295,7 +300,7 @@ impl<'a> Trade<'a> {
);
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)) {
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_img = match item.quality() {
Quality::Low => self.imgs.inv_slot_grey,

View File

@ -1,35 +1,53 @@
use common::comp::item::{
armor::{Armor, ArmorKind, Protection},
tool::{Hands, Tool, ToolKind},
Item, ItemDesc, ItemKind, ModularComponent,
tool::{Hands, StatKind, Stats, Tool, ToolKind},
Item, ItemDesc, ItemKind, MaterialStatManifest, ModularComponent,
};
use std::{borrow::Cow, fmt::Write};
pub fn loadout_slot_text<'a>(
item: Option<&'a impl ItemDesc>,
mut empty: impl FnMut() -> (&'a str, &'a str),
msm: &'a MaterialStatManifest,
) -> (&'a str, Cow<'a, str>) {
item.map_or_else(
|| {
let (title, desc) = empty();
(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() {
ItemKind::Armor(armor) => {
Cow::Owned(armor_desc(armor, item.description(), item.num_slots()))
},
ItemKind::Tool(tool) => Cow::Owned(tool_desc(&tool, item.components(), item.description())),
ItemKind::ModularComponent(mc) => Cow::Owned(modular_component_desc(mc)),
ItemKind::Tool(tool) => Cow::Owned(tool_desc(
&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::Consumable { .. } => Cow::Owned(consumable_desc(item.description())),
ItemKind::Throwable { .. } => Cow::Owned(throwable_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::TagExamples { .. } => 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
fn modular_component_desc(mc: &ModularComponent) -> String {
format!("Modular Component\n\n{:?}", mc)
fn modular_component_desc(
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) }
@ -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 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) }
@ -102,7 +143,7 @@ fn armor_desc(armor: &Armor, desc: &str, slots: u16) -> String {
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 {
ToolKind::Sword => "Sword",
ToolKind::Axe => "Axe",
@ -119,26 +160,16 @@ fn tool_desc(tool: &Tool, components: &[Item], desc: &str) -> String {
};
// 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 hands = match tool.hands {
Hands::One => "One",
Hands::Two => "Two",
};
let speed = tool.base_speed(components);
let mut result = format!(
"{}-Handed {}\n\nDPS: {: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>",
hands,
kind,
speed * power * 10.0, // Damage per second
power * 10.0,
//poise_strength * 10.0,
speed
);
let mut result = format!("{}-Handed {}\n\n", hands, kind);
result += &statblock_desc(&stats);
if !components.is_empty() {
result += "Made from:\n";
for component in components {
@ -154,6 +185,19 @@ fn tool_desc(tool: &Tool, components: &[Item], desc: &str) -> String {
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)]
mod tests {
use super::*;
@ -200,11 +244,29 @@ mod tests {
#[test]
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!(
"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
)
);
}