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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,25 +87,11 @@ pub trait TagExampleInfo {
fn exemplar_identifier(&self) -> &'static str;
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum ItemTag {
ClothItem,
ModularComponent(ModularComponentTag),
MetalIngot(f32),
}
// The PartialEq implementation for ItemTag is used for determining whether a
// RecipeInput matches
impl PartialEq for ItemTag {
fn eq(&self, other: &Self) -> bool {
use ItemTag::*;
match (self, other) {
(ClothItem, ClothItem) => true,
(ModularComponent(a), ModularComponent(b)) => a == b,
(MetalIngot(_), MetalIngot(_)) => true,
_ => false,
}
}
MetalIngot,
}
impl TagExampleInfo for ItemTag {
@ -113,7 +99,7 @@ impl TagExampleInfo for ItemTag {
match self {
ItemTag::ClothItem => "cloth item",
ItemTag::ModularComponent(kind) => kind.name(),
ItemTag::MetalIngot(_) => "metal ingot",
ItemTag::MetalIngot => "metal ingot",
}
}
@ -121,7 +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",
ItemTag::MetalIngot => "common.items.tag_examples.metal_ingot",
}
}
}
@ -229,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,
@ -389,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 {
@ -405,7 +397,7 @@ impl Item {
item_def: inner_item,
item_config: None,
};
item.update_item_config();
item.update_item_config(msm);
item
}
@ -413,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
@ -426,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
@ -494,21 +488,22 @@ impl Item {
}
}
pub fn add_component(&mut self, component: Item) {
pub fn add_component(&mut self, component: Item, msm: &MaterialStatManifest) {
// TODO: hook for typechecking (not needed atm if this is only used by DB
// 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

View File

@ -2,17 +2,13 @@
// version in voxygen\src\meta.rs in order to reset save files to being empty
use crate::{
assets::{self, Asset},
comp::{
item::{ItemDesc, ItemKind, ItemTag},
skills::Skill,
CharacterAbility, Item,
},
assets::{self, Asset, AssetExt},
comp::{item::ItemKind, skills::Skill, CharacterAbility, Item},
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::{
ops::{AddAssign, MulAssign},
ops::{AddAssign, DivAssign, MulAssign},
time::Duration,
};
use tracing::error;
@ -78,6 +74,12 @@ impl Stats {
}
}
impl Asset for Stats {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
impl AddAssign<Stats> for Stats {
fn add_assign(&mut self, other: Stats) {
self.equip_time_millis += other.equip_time_millis;
@ -86,11 +88,43 @@ impl AddAssign<Stats> for Stats {
self.speed += other.speed;
}
}
impl MulAssign<f32> for Stats {
fn mul_assign(&mut self, scalar: f32) {
self.power *= scalar;
self.poise_strength *= scalar;
self.speed *= scalar;
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(HashMap<String, Stats>);
// This could be a Compound that also loads the keys, but the RecipeBook
// Compound impl already does that, so checking for existence here is redundant.
impl Asset for MaterialStatManifest {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
impl Default for MaterialStatManifest {
fn default() -> MaterialStatManifest {
MaterialStatManifest::load_expect_cloned("common.material_stats_manifest")
}
}
@ -101,38 +135,36 @@ pub enum StatKind {
}
impl StatKind {
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 best_multiplier: Option<f32> = None;
let mut multipliers: Vec<Stats> = Vec::new();
for item in components.iter() {
match item.kind() {
ItemKind::ModularComponent(mc) => {
let inner_stats = StatKind::Direct(mc.stats).resolve_stats(item.components());
let inner_stats =
StatKind::Direct(mc.stats).resolve_stats(msm, item.components());
stats += inner_stats;
},
ItemKind::Ingredient { .. } => {
for tag in item.tags() {
// exhaustive match to ensure that new tags get stats counted
match tag {
ItemTag::ClothItem => {},
ItemTag::ModularComponent(_) => {},
ItemTag::MetalIngot(multiplier) => {
best_multiplier =
Some(best_multiplier.unwrap_or(*multiplier).max(*multiplier));
},
}
if let Some(mult_stats) = msm.0.get(item.item_definition_id()) {
multipliers.push(*mult_stats);
}
},
// TODO: add stats from enhancement slots, unless those end up as tagged
// ingredients
// TODO: add stats from enhancement slots
_ => (),
}
}
if let Some(multiplier) = best_multiplier {
stats *= multiplier;
// 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;
}
// if an item has 0.0 speed, that panics due to being infinite duration, so
// enforce speed >= 0.1
@ -141,9 +173,9 @@ impl StatKind {
}
}
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);
let (power, speed) = match tool.hands {
Hands::One => (0.67, 1.33),
// TODO: Restore this when one-handed weapons are made accessible
@ -204,29 +236,30 @@ 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).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",
@ -245,8 +278,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 {

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)
{

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,
}
}
}
@ -623,7 +626,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

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

View File

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