Durability system in place

This commit is contained in:
Sam 2022-06-06 17:48:39 -04:00
parent 73cd4e5136
commit df13741be9
10 changed files with 146 additions and 66 deletions

View File

@ -1230,7 +1230,7 @@ fn weapon_rating<T: ItemDesc>(item: &T, _msm: &MaterialStatManifest) -> f32 {
const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
let rating = if let ItemKind::Tool(tool) = &*item.kind() {
let stats = tool.stats;
let stats = tool.stats(item.stats_durability_multiplier());
// TODO: Look into changing the 0.5 to reflect armor later maybe?
// Since it is only for weapon though, it probably makes sense to leave
@ -1361,7 +1361,9 @@ pub fn compute_crit_mult(inventory: Option<&Inventory>, msm: &MaterialStatManife
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).crit_power
armor
.stats(msm, item.stats_durability_multiplier())
.crit_power
} else {
None
}
@ -1379,7 +1381,9 @@ pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialSt
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).energy_reward
armor
.stats(msm, item.stats_durability_multiplier())
.energy_reward
} else {
None
}
@ -1397,7 +1401,9 @@ pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatM
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).energy_max
armor
.stats(msm, item.stats_durability_multiplier())
.energy_max
} else {
None
}
@ -1433,7 +1439,7 @@ pub fn stealth_multiplier_from_items(
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).stealth
armor.stats(msm, item.stats_durability_multiplier()).stealth
} else {
None
}
@ -1456,7 +1462,9 @@ pub fn compute_protection(
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).protection
armor
.stats(msm, item.stats_durability_multiplier())
.protection
} else {
None
}

View File

@ -1,5 +1,5 @@
use crate::{
comp::item::{MaterialStatManifest, Rgb},
comp::item::{DurabilityMultiplier, MaterialStatManifest, Rgb},
terrain::{Block, BlockKind},
};
use serde::{Deserialize, Serialize};
@ -212,8 +212,12 @@ pub struct Armor {
impl Armor {
pub fn new(kind: ArmorKind, stats: StatsSource) -> Self { Self { kind, stats } }
pub fn stats(&self, msm: &MaterialStatManifest) -> Stats {
match &self.stats {
pub fn stats(
&self,
msm: &MaterialStatManifest,
durability_multiplier: DurabilityMultiplier,
) -> Stats {
let base_stats = match &self.stats {
StatsSource::Direct(stats) => *stats,
StatsSource::FromSet(set) => {
let set_stats = msm.armor_stats(set).unwrap_or_else(Stats::none);
@ -237,7 +241,9 @@ impl Armor {
set_stats * multiplier
},
}
};
let DurabilityMultiplier(mult) = durability_multiplier;
base_stats * mult
}
#[cfg(test)]

View File

@ -396,6 +396,10 @@ pub struct Item {
slots: Vec<InvSlot>,
item_config: Option<Box<ItemConfig>>,
hash: u64,
/// Tracks how many deaths occurred while item was equipped, which is
/// converted into the items durability. Only tracked for tools and armor
/// currently.
durability: Option<u32>,
}
use std::hash::{Hash, Hasher};
@ -609,7 +613,8 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
};
let abilities = if let Some(set_key) = item.ability_spec() {
if let Some(set) = ability_map.get_ability_set(&set_key) {
set.clone().modified_by_tool(tool)
set.clone()
.modified_by_tool(tool, item.stats_durability_multiplier())
} else {
error!(
"Custom ability set: {:?} references non-existent set, falling back to \
@ -619,7 +624,8 @@ impl TryFrom<(&Item, &AbilityMap, &MaterialStatManifest)> for ItemConfig {
tool_default(tool.kind).cloned().unwrap_or_default()
}
} else if let Some(set) = tool_default(tool.kind) {
set.clone().modified_by_tool(tool)
set.clone()
.modified_by_tool(tool, item.stats_durability_multiplier())
} else {
error!(
"No ability set defined for tool: {:?}, falling back to default ability set.",
@ -785,6 +791,7 @@ impl Item {
// These fields are updated immediately below
item_config: None,
hash: 0,
durability: None,
};
item.update_item_state(ability_map, msm);
item
@ -1089,7 +1096,7 @@ impl Item {
ItemBase::Modular(mod_base) => {
// TODO: Try to move further upward
let msm = MaterialStatManifest::load().read();
mod_base.kind(self.components(), &msm)
mod_base.kind(self.components(), &msm, self.stats_durability_multiplier())
},
}
}
@ -1188,6 +1195,18 @@ impl Item {
}
}
pub fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
const MAX_DURABILITY: u32 = 8;
let durability = self.durability.map_or(0, |x| x.clamp(0, MAX_DURABILITY));
const DURABILITY_THRESHOLD: u32 = 4;
const MIN_FRAC: f32 = 0.25;
let mult = durability.saturating_sub(DURABILITY_THRESHOLD) as f32
/ (MAX_DURABILITY - DURABILITY_THRESHOLD) as f32
* (1.0 - MIN_FRAC)
+ MIN_FRAC;
DurabilityMultiplier(mult)
}
#[cfg(test)]
pub fn create_test_item_from_kind(kind: ItemKind) -> Self {
let ability_map = &AbilityMap::load().read();
@ -1211,10 +1230,9 @@ pub trait ItemDesc {
fn num_slots(&self) -> u16;
fn item_definition_id(&self) -> ItemDefinitionId<'_>;
fn tags(&self) -> Vec<ItemTag>;
fn is_modular(&self) -> bool;
fn components(&self) -> &[Item];
fn stats_durability_multiplier(&self) -> DurabilityMultiplier;
fn tool_info(&self) -> Option<ToolKind> {
if let ItemKind::Tool(tool) = &*self.kind() {
@ -1243,6 +1261,10 @@ impl ItemDesc for Item {
fn is_modular(&self) -> bool { self.is_modular() }
fn components(&self) -> &[Item] { self.components() }
fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
self.stats_durability_multiplier()
}
}
impl ItemDesc for ItemDef {
@ -1265,6 +1287,8 @@ impl ItemDesc for ItemDef {
fn is_modular(&self) -> bool { false }
fn components(&self) -> &[Item] { &[] }
fn stats_durability_multiplier(&self) -> DurabilityMultiplier { DurabilityMultiplier(1.0) }
}
impl Component for Item {
@ -1278,6 +1302,9 @@ impl Component for ItemDrop {
type Storage = DenseVecStorage<Self>;
}
#[derive(Copy, Clone, Debug)]
pub struct DurabilityMultiplier(pub f32);
impl<'a, T: ItemDesc + ?Sized> ItemDesc for &'a T {
fn description(&self) -> &str { (*self).description() }
@ -1296,6 +1323,10 @@ impl<'a, T: ItemDesc + ?Sized> ItemDesc for &'a T {
fn is_modular(&self) -> bool { (*self).is_modular() }
fn components(&self) -> &[Item] { (*self).components() }
fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
(*self).stats_durability_multiplier()
}
}
/// Returns all item asset specifiers

View File

@ -1,7 +1,8 @@
use super::{
armor,
tool::{self, AbilityMap, AbilitySpec, Hands},
Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, Material, Quality, ToolKind,
tool::{self, AbilityMap, AbilitySpec, Hands, Tool},
DurabilityMultiplier, Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, Material, Quality,
ToolKind,
};
use crate::{
assets::{self, Asset, AssetExt, AssetHandle},
@ -98,8 +99,17 @@ impl ModularBase {
hand_restriction.unwrap_or(Hands::One)
}
pub fn kind(&self, components: &[Item], msm: &MaterialStatManifest) -> Cow<ItemKind> {
fn resolve_stats(components: &[Item], msm: &MaterialStatManifest) -> tool::Stats {
pub(super) fn kind(
&self,
components: &[Item],
msm: &MaterialStatManifest,
durability_multiplier: DurabilityMultiplier,
) -> Cow<ItemKind> {
fn resolve_stats(
components: &[Item],
msm: &MaterialStatManifest,
durability_multiplier: DurabilityMultiplier,
) -> tool::Stats {
components
.iter()
.filter_map(|comp| {
@ -110,6 +120,7 @@ impl ModularBase {
}
})
.fold(tool::Stats::one(), |a, b| a * b)
* durability_multiplier
}
let toolkind = components
@ -124,11 +135,11 @@ impl ModularBase {
.unwrap_or(ToolKind::Empty);
match self {
ModularBase::Tool => Cow::Owned(ItemKind::Tool(tool::Tool {
kind: toolkind,
hands: Self::resolve_hands(components),
stats: resolve_stats(components, msm),
})),
ModularBase::Tool => Cow::Owned(ItemKind::Tool(Tool::new(
toolkind,
Self::resolve_hands(components),
resolve_stats(components, msm, durability_multiplier),
))),
}
}

View File

@ -3,14 +3,13 @@
use crate::{
assets::{self, Asset, AssetExt, AssetHandle},
comp::{ability::Stance, skills::Skill, CharacterAbility, SkillSet},
comp::{
ability::Stance, item::DurabilityMultiplier, skills::Skill, CharacterAbility, SkillSet,
},
};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::{
ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub},
time::Duration,
};
use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
pub enum ToolKind {
@ -171,9 +170,11 @@ impl Add<Stats> for Stats {
}
}
}
impl AddAssign<Stats> for Stats {
fn add_assign(&mut self, other: Stats) { *self = *self + other; }
}
impl Sub<Stats> for Stats {
type Output = Self;
@ -190,6 +191,7 @@ impl Sub<Stats> for Stats {
}
}
}
impl Mul<Stats> for Stats {
type Output = Self;
@ -206,9 +208,29 @@ impl Mul<Stats> for Stats {
}
}
}
impl MulAssign<Stats> for Stats {
fn mul_assign(&mut self, other: Stats) { *self = *self * other; }
}
impl Mul<DurabilityMultiplier> for Stats {
type Output = Self;
fn mul(self, value: DurabilityMultiplier) -> Self {
let DurabilityMultiplier(value) = value;
Self {
equip_time_secs: self.equip_time_secs / value.max(0.01),
power: self.power * value,
effect_power: self.effect_power * value,
speed: self.speed * value,
crit_chance: self.crit_chance * value,
range: self.range * value,
energy_efficiency: self.energy_efficiency * value,
buff_strength: self.buff_strength * value,
}
}
}
impl Div<f32> for Stats {
type Output = Self;
@ -225,15 +247,12 @@ impl Div<f32> for Stats {
}
}
}
impl DivAssign<usize> for Stats {
fn div_assign(&mut self, scalar: usize) { *self = *self / (scalar as f32); }
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Tool {
pub kind: ToolKind,
pub hands: Hands,
pub stats: Stats,
stats: Stats,
// TODO: item specific abilities
}
@ -259,24 +278,9 @@ impl Tool {
}
}
// Keep power between 0.5 and 2.00
pub fn base_power(&self) -> f32 { self.stats.power }
pub fn base_effect_power(&self) -> f32 { self.stats.effect_power }
/// Has floor to prevent infinite durations being created later down due to
/// a divide by zero
pub fn base_speed(&self) -> f32 { self.stats.speed.max(0.1) }
pub fn base_crit_chance(&self) -> f32 { self.stats.crit_chance }
pub fn base_range(&self) -> f32 { self.stats.range }
pub fn base_energy_efficiency(&self) -> f32 { self.stats.energy_efficiency }
pub fn base_buff_strength(&self) -> f32 { self.stats.buff_strength }
pub fn equip_time(&self) -> Duration { Duration::from_secs_f32(self.stats.equip_time_secs) }
pub fn stats(&self, durability_multiplier: DurabilityMultiplier) -> Stats {
self.stats * durability_multiplier
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -376,10 +380,16 @@ impl AbilityContext {
impl AbilitySet<AbilityItem> {
#[must_use]
pub fn modified_by_tool(self, tool: &Tool) -> Self {
pub fn modified_by_tool(
self,
tool: &Tool,
durability_multiplier: DurabilityMultiplier,
) -> Self {
self.map(|a| AbilityItem {
id: a.id,
ability: a.ability.adjusted_by_stats(tool.stats),
ability: a
.ability
.adjusted_by_stats(tool.stats(durability_multiplier)),
})
}
}

View File

@ -270,7 +270,9 @@ impl Poise {
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).poise_resilience
armor
.stats(msm, item.stats_durability_multiplier())
.poise_resilience
} else {
None
}

View File

@ -335,7 +335,10 @@ pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
footwear = data.inventory.and_then(|inv| {
inv.equipped(EquipSlot::Armor(ArmorSlot::Feet))
.map(|armor| match armor.kind().as_ref() {
ItemKind::Armor(a) => a.stats(data.msm).ground_contact,
ItemKind::Armor(a) => {
a.stats(data.msm, armor.stats_durability_multiplier())
.ground_contact
},
_ => Friction::Normal,
})
});
@ -719,7 +722,10 @@ pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
data.inventory
.and_then(|inv| inv.equipped(equip_slot))
.and_then(|item| match &*item.kind() {
ItemKind::Tool(tool) => Some(tool.equip_time()),
ItemKind::Tool(tool) => Some(Duration::from_secs_f32(
tool.stats(item.stats_durability_multiplier())
.equip_time_secs,
)),
_ => None,
})
};
@ -1261,7 +1267,7 @@ pub fn get_crit_data(data: &JoinData<'_>, ai: AbilityInfo) -> (f32, f32) {
.and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
.and_then(|item| {
if let ItemKind::Tool(tool) = &*item.kind() {
Some(tool.base_crit_chance())
Some(tool.stats(item.stats_durability_multiplier()).crit_chance)
} else {
None
}
@ -1282,7 +1288,7 @@ pub fn get_tool_stats(data: &JoinData<'_>, ai: AbilityInfo) -> tool::Stats {
.and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
.and_then(|item| {
if let ItemKind::Tool(tool) = &*item.kind() {
Some(tool.stats)
Some(tool.stats(item.stats_durability_multiplier()))
} else {
None
}

View File

@ -1211,7 +1211,9 @@ impl<'a> Widget for Diary<'a> {
.inventory
.equipped(EquipSlot::ActiveMainhand)
.and_then(|item| match &*item.kind() {
ItemKind::Tool(tool) => Some(tool.stats),
ItemKind::Tool(tool) => {
Some(tool.stats(item.stats_durability_multiplier()))
},
_ => None,
});
@ -1219,7 +1221,9 @@ impl<'a> Widget for Diary<'a> {
.inventory
.equipped(EquipSlot::ActiveOffhand)
.and_then(|item| match &*item.kind() {
ItemKind::Tool(tool) => Some(tool.stats),
ItemKind::Tool(tool) => {
Some(tool.stats(item.stats_durability_multiplier()))
},
_ => None,
});

View File

@ -102,7 +102,7 @@ pub fn material_kind_text<'a>(kind: &MaterialKind, i18n: &'a Localization) -> Co
pub fn stats_count(item: &dyn ItemDesc, msm: &MaterialStatManifest) -> usize {
match &*item.kind() {
ItemKind::Armor(armor) => {
let armor_stats = armor.stats(msm);
let armor_stats = armor.stats(msm, item.stats_durability_multiplier());
armor_stats.energy_reward.is_some() as usize
+ armor_stats.energy_max.is_some() as usize
+ armor_stats.stealth.is_some() as usize

View File

@ -583,7 +583,7 @@ impl<'a> Widget for ItemTooltip<'a> {
// Stats
match &*item.kind() {
ItemKind::Tool(tool) => {
let stats = tool.stats;
let stats = tool.stats(item.stats_durability_multiplier());
// Power
widget::Text::new(&format!(
@ -671,8 +671,9 @@ impl<'a> Widget for ItemTooltip<'a> {
if let Some(equipped_item) = equipped_item {
if let ItemKind::Tool(equipped_tool) = &*equipped_item.kind() {
let tool_stats = tool.stats;
let equipped_tool_stats = equipped_tool.stats;
let tool_stats = tool.stats(item.stats_durability_multiplier());
let equipped_tool_stats =
equipped_tool.stats(equipped_item.stats_durability_multiplier());
let diff = tool_stats - equipped_tool_stats;
let power_diff =
util::comparison(tool_stats.power, equipped_tool_stats.power);
@ -756,7 +757,7 @@ impl<'a> Widget for ItemTooltip<'a> {
}
},
ItemKind::Armor(armor) => {
let armor_stats = armor.stats(self.msm);
let armor_stats = armor.stats(self.msm, item.stats_durability_multiplier());
let mut stat_text = |text: String, i: usize| {
widget::Text::new(&text)
@ -877,7 +878,8 @@ impl<'a> Widget for ItemTooltip<'a> {
if let Some(equipped_item) = equipped_item {
if let ItemKind::Armor(equipped_armor) = &*equipped_item.kind() {
let equipped_stats = equipped_armor.stats(self.msm);
let equipped_stats = equipped_armor
.stats(self.msm, equipped_item.stats_durability_multiplier());
let diff = armor_stats - equipped_stats;
let protection_diff = util::option_comparison(
&armor_stats.protection,