Merge branch 'sam/durability' into 'master'

Durability

See merge request veloren/veloren!3509
This commit is contained in:
Samuel Keiffer 2023-04-06 22:42:52 +00:00
commit 162509e1c9
92 changed files with 2488 additions and 524 deletions

View File

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Custom spots can be added without recompilation (only ron and vox files)
- Setting in userdata/server/server_config/settings.ron that controls the length of each day/night cycle.
- Starting site can now be chosen during character creation
- Durability loss of equipped items on death
### Changed
- Bats move slower and use a simple proportional controller to maintain altitude

View File

@ -25,7 +25,7 @@
craft_sprite: Some(CraftingBench),
),
"velorite_frag": (
output: ("common.items.mineral.ore.veloritefrag", 2),
output: ("common.items.mineral.ore.veloritefrag", 3),
inputs: [
(Item("common.items.mineral.ore.velorite"), 1, false),
(Item("common.items.tool.craftsman_hammer"), 0, false),

File diff suppressed because it is too large Load Diff

View File

@ -11,10 +11,10 @@
(0.08, Item("common.items.mineral.gem.ruby")),
(0.1, Item("common.items.mineral.gem.emerald")),
(0.2, Item("common.items.mineral.gem.sapphire")),
(0.25, Item("common.items.mineral.ore.velorite")),
(0.5, Item("common.items.mineral.ore.veloritefrag")),
(0.5, Item("common.items.mineral.ore.velorite")),
(0.75, Item("common.items.mineral.gem.topaz")),
(1.0, Item("common.items.mineral.gem.amethyst")),
(2.0, Item("common.items.mineral.ore.veloritefrag")),
(4.0, Item("common.items.crafting_ing.stones")),
// Ores

View File

@ -103,6 +103,7 @@ common-stats-energy_reward = Energy Reward
common-stats-crit_power = Crit Power
common-stats-stealth = Stealth
common-stats-slots = Slots
common-stats-durability = Durability
common-material-metal = Metal
common-material-wood = Wood
common-material-stone = Stone

View File

@ -3,6 +3,9 @@ hud-crafting-recipes = Recipes
hud-crafting-ingredients = Ingredients:
hud-crafting-craft = Craft
hud-crafting-craft_all = Craft All
hud-crafting-repair = Repair
hud-crafting-repair_equipped = Repair Equipped
hud-crafting-repair_all = Repair All
hud-crafting-tool_cata = Requires:
hud-crafting-req_crafting_station = Requires:
hud-crafting-anvil = Anvil
@ -14,6 +17,7 @@ hud-crafting-loom = Loom
hud-crafting-spinning_wheel = Spinning Wheel
hud-crafting-tanning_rack = Tanning Rack
hud-crafting-salvaging_station = Salvaging Bench
hud-crafting-repair_bench = Repair Bench
hud-crafting-campfire = Campfire
hud-crafting-tabs-all = All
hud-crafting-tabs-armor = Armor
@ -30,7 +34,7 @@ hud-crafting-dismantle_title = Dismantling
hud-crafting-dismantle_explanation =
Hover items in your bag to see what
you can salvage.
Double-Click them to start dismantling.
hud-crafting-modular_desc = Drag Item-Parts here to craft a weapon
hud-crafting-mod_weap_prim_slot_title = Primary Weapon Component
@ -42,4 +46,6 @@ hud-crafting-mod_comp_metal_prim_slot_desc = Place a metal ingot here, only cert
hud-crafting-mod_comp_wood_prim_slot_title = Wood
hud-crafting-mod_comp_wood_prim_slot_desc = Place a kind of wood here, only certain woods can be used to make weapons.
hud-crafting-mod_comp_sec_slot_title = Animal Material
hud-crafting-mod_comp_sec_slot_desc = Optionally place an animal crafting ingredient, only certain ingredients can be used to augment weapons.
hud-crafting-mod_comp_sec_slot_desc = Optionally place an animal crafting ingredient, only certain ingredients can be used to augment weapons.
hud-crafting-repair_slot_title = Damaged Item
hud-crafting-repair_slot_desc = Place an item here to see the cost of repairing it at its current durability level.

View File

@ -13,6 +13,7 @@ hud-press_key_to_toggle_lantern_fmt = [{ $key }] Lantern
hud-press_key_to_show_debug_info_fmt = Press { $key } to show debug info
hud-press_key_to_toggle_keybindings_fmt = Press { $key } to toggle keybindings
hud-press_key_to_toggle_debug_info_fmt = Press { $key } to toggle debug info
hud_items_lost_dur = Your equipped items have lost Durability.
hud-press_key_to_respawn = Press { $key } to respawn at the last campfire you visited.
hud-tutorial_btn = Tutorial
hud-tutorial_click_here = Press [ { $key } ] to free your cursor and click this button!

View File

@ -39,6 +39,10 @@
"voxel.sprite.salvaging_station.salvaging_station-0",
(0.0, 0.0, 0.0), (-50.0, 40.0, 30.0), 0.9,
),
Simple("RepairBench"): VoxTrans(
"voxel.sprite.repair_bench.repair_bench-0",
(0.0, 0.0, 0.0), (-50.0, 40.0, 30.0), 0.9,
),
// Weapons
// Diary Example Images
Simple("example_utility"): VoxTrans(
@ -3155,7 +3159,7 @@
(0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
),
Simple("common.items.mineral.ore.veloritefrag"): VoxTrans(
"voxel.sprite.velorite.velorite_1",
"voxel.sprite.velorite.velorite",
(0.0, 0.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
),
Simple("common.items.food.apple_mushroom_curry"): VoxTrans(

View File

@ -447,7 +447,7 @@
color: None
),
(Danari, Male, "common.items.armor.misc.head.straw"): (
vox_spec: ("armor.misc.head.straw", (-2.0, -5.0, 7.0)),
vox_spec: ("armor.misc.head.straw", (-2.0, -4.0, 7.0)),
color: None
),
(Danari, Female, "common.items.armor.misc.head.straw"): (
@ -463,7 +463,7 @@
color: None
),
(Orc, Male, "common.items.armor.misc.head.straw"): (
vox_spec: ("armor.misc.head.straw", (-3.0, -3.0, 8.0)),
vox_spec: ("armor.misc.head.straw", (-3.0, -4.0, 7.0)),
color: None
),
(Orc, Female, "common.items.armor.misc.head.straw"): (

View File

@ -800,7 +800,7 @@
Simple("common.items.food.blue_cheese"): "voxel.object.blue_cheese",
Simple("common.items.food.mushroom"): "voxel.sprite.mushrooms.mushroom-10",
Simple("common.items.mineral.ore.velorite"): "voxel.sprite.velorite.velorite_ore",
Simple("common.items.mineral.ore.veloritefrag"): "voxel.sprite.velorite.velorite_1",
Simple("common.items.mineral.ore.veloritefrag"): "voxel.sprite.velorite.velorite",
Simple("common.items.food.apple_mushroom_curry"): "voxel.object.mushroom_curry",
Simple("common.items.food.spore_corruption"): "voxel.sprite.spore.corruption_spore",
Simple("common.items.food.apple_stick"): "voxel.object.apple_stick",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/repair_bench/repair_bench-0.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/velorite/velorite.vox (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -716,7 +716,7 @@ Velorite: Some((
variations: [
(
model: "voxygen.voxel.sprite.velorite.velorite_ore",
offset: (-5.0, -5.0, -5.0),
offset: (-5.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
],
@ -725,53 +725,8 @@ Velorite: Some((
VeloriteFrag: Some((
variations: [
(
model: "voxygen.voxel.sprite.velorite.velorite_1",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_2",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_3",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_4",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_5",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_6",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_7",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_8",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_9",
offset: (-3.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
(
model: "voxygen.voxel.sprite.velorite.velorite_10",
offset: (-3.0, -5.0, 0.0),
model: "voxygen.voxel.sprite.velorite.velorite",
offset: (-4.0, -5.0, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
],
@ -784,7 +739,7 @@ Chest: Some((
model: "voxygen.voxel.sprite.chests.chest",
offset: (-7.0, -5.0, -0.0),
lod_axes: (1.0, 1.0, 1.0),
),
),
(
model: "voxygen.voxel.sprite.chests.chest_dark",
offset: (-7.0, -5.0, -0.0),
@ -799,13 +754,13 @@ Chest: Some((
wind_sway: 0.0,
)),
CommonLockedChest: Some((
variations: [
variations: [
(
model: "voxygen.voxel.sprite.chests.chest_gold",
offset: (-7.0, -5.0, -0.0),
lod_axes: (1.0, 1.0, 1.0),
),
],
wind_sway: 0.0,
)),
@ -3788,6 +3743,17 @@ CookingPot: Some((
],
wind_sway: 0.0,
)),
// Repair Bench
RepairBench: Some((
variations: [
(
model: "voxygen.voxel.sprite.repair_bench.repair_bench-0",
offset: (-7.0, -8.0, 0.0),
lod_axes: (0.0, 0.0, 0.0),
),
],
wind_sway: 0.0,
)),
// Ensnaring Vines
EnsnaringVines: Some((
variations: [

View File

@ -38,7 +38,7 @@ use common::{
lod,
mounting::Rider,
outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook},
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{GameMode, PlayerEntity, Time, TimeOfDay},
shared_server_config::ServerConstants,
spiral::Spiral2d,
@ -222,6 +222,7 @@ pub struct Client {
pub chat_mode: ChatMode,
recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook,
repair_recipe_book: RepairRecipeBook,
available_recipes: HashMap<String, Option<SpriteKind>>,
lod_zones: HashMap<Vec2<i32>, lod::Zone>,
lod_last_requested: Option<Instant>,
@ -362,6 +363,7 @@ impl Client {
material_stats,
ability_map,
server_constants,
repair_recipe_book,
} = loop {
tokio::select! {
// Spawn in a blocking thread (leaving the network thread free). This is mostly
@ -655,6 +657,7 @@ impl Client {
world_map.pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
max_group_size,
client_timeout,
))
@ -670,6 +673,7 @@ impl Client {
pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
max_group_size,
client_timeout,
) = loop {
@ -708,6 +712,7 @@ impl Client {
pois,
recipe_book,
component_recipe_book,
repair_recipe_book,
available_recipes: HashMap::default(),
chat_mode: ChatMode::default(),
@ -1146,6 +1151,8 @@ impl Client {
pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book }
pub fn repair_recipe_book(&self) -> &RepairRecipeBook { &self.repair_recipe_book }
pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
&self.available_recipes
}
@ -1289,6 +1296,39 @@ impl Client {
)));
}
/// Repairs the item in the given inventory slot. `sprite_pos` should be
/// the location of a relevant crafting station within range of the player.
pub fn repair_item(
&mut self,
item: Slot,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: Vec3<i32>,
) -> bool {
let is_repairable = {
let inventories = self.inventories();
let inventory = inventories.get(self.entity());
inventory.map_or(false, |inv| {
if let Some(item) = match item {
Slot::Equip(equip_slot) => inv.equipped(equip_slot),
Slot::Inventory(invslot) => inv.get(invslot),
} {
item.has_durability()
} else {
false
}
})
};
if is_repairable {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
craft_event: CraftEvent::Repair { item, slots },
craft_sprite: Some(sprite_pos),
},
)));
}
is_repairable
}
fn update_available_recipes(&mut self) {
self.available_recipes = self
.recipe_book

View File

@ -10,7 +10,7 @@ use common::{
event::UpdateCharacterMetadata,
lod,
outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook},
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{Time, TimeOfDay},
shared_server_config::ServerConstants,
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
@ -65,6 +65,7 @@ pub enum ServerInit {
world_map: crate::msg::world_msg::WorldMapMsg,
recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook,
repair_recipe_book: RepairRecipeBook,
material_stats: MaterialStatManifest,
ability_map: comp::item::tool::AbilityMap,
server_constants: ServerConstants,

View File

@ -15,7 +15,7 @@ use veloren_common::{
armor::{ArmorKind, Protection},
modular::{generate_weapon_primary_components, generate_weapons},
tool::{Hands, Tool, ToolKind},
Item, MaterialStatManifest,
DurabilityMultiplier, Item, MaterialStatManifest,
},
},
generation::{EntityConfig, EntityInfo},
@ -58,21 +58,23 @@ fn armor_stats() -> Result<(), Box<dyn Error>> {
}
let msm = &MaterialStatManifest::load().read();
let dur_mult = DurabilityMultiplier(1.0);
let armor_stats = armor.stats(msm, dur_mult);
let protection = match armor.stats(msm).protection {
let protection = match armor_stats.protection {
Some(Protection::Invincible) => "Invincible".to_string(),
Some(Protection::Normal(value)) => value.to_string(),
None => "0.0".to_string(),
};
let poise_resilience = match armor.stats(msm).poise_resilience {
let poise_resilience = match armor_stats.poise_resilience {
Some(Protection::Invincible) => "Invincible".to_string(),
Some(Protection::Normal(value)) => value.to_string(),
None => "0.0".to_string(),
};
let max_energy = armor.stats(msm).energy_max.unwrap_or(0.0).to_string();
let energy_reward = armor.stats(msm).energy_reward.unwrap_or(0.0).to_string();
let crit_power = armor.stats(msm).crit_power.unwrap_or(0.0).to_string();
let stealth = armor.stats(msm).stealth.unwrap_or(0.0).to_string();
let max_energy = armor_stats.energy_max.unwrap_or(0.0).to_string();
let energy_reward = armor_stats.energy_reward.unwrap_or(0.0).to_string();
let crit_power = armor_stats.crit_power.unwrap_or(0.0).to_string();
let stealth = armor_stats.stealth.unwrap_or(0.0).to_string();
wtr.write_record([
item.item_definition_id()
@ -124,14 +126,17 @@ fn weapon_stats() -> Result<(), Box<dyn Error>> {
for item in items.iter() {
if let comp::item::ItemKind::Tool(tool) = &*item.kind() {
let power = tool.base_power().to_string();
let effect_power = tool.base_effect_power().to_string();
let speed = tool.base_speed().to_string();
let crit_chance = tool.base_crit_chance().to_string();
let range = tool.base_range().to_string();
let energy_efficiency = tool.base_energy_efficiency().to_string();
let buff_strength = tool.base_buff_strength().to_string();
let equip_time = tool.equip_time().as_secs_f32().to_string();
let dur_mult = DurabilityMultiplier(1.0);
let tool_stats = tool.stats(dur_mult);
let power = tool_stats.power.to_string();
let effect_power = tool_stats.effect_power.to_string();
let speed = tool_stats.speed.to_string();
let crit_chance = tool_stats.crit_chance.to_string();
let range = tool_stats.range.to_string();
let energy_efficiency = tool_stats.energy_efficiency.to_string();
let buff_strength = tool_stats.buff_strength.to_string();
let equip_time = tool_stats.equip_time_secs.to_string();
let kind = get_tool_kind(&tool.kind);
let hands = get_tool_hands(tool);

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

@ -105,6 +105,10 @@ pub enum CraftEvent {
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
},
Repair {
item: Slot,
slots: Vec<(u32, InvSlotId)>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -1,5 +1,5 @@
use crate::{
comp::item::{MaterialStatManifest, Rgb},
comp::item::{DurabilityMultiplier, MaterialStatManifest, Rgb},
terrain::{Block, BlockKind},
};
use serde::{Deserialize, Serialize};
@ -25,6 +25,25 @@ pub enum ArmorKind {
Bag,
}
impl ArmorKind {
pub fn has_durability(self) -> bool {
match self {
ArmorKind::Shoulder => true,
ArmorKind::Chest => true,
ArmorKind::Belt => true,
ArmorKind::Hand => true,
ArmorKind::Pants => true,
ArmorKind::Foot => true,
ArmorKind::Back => true,
ArmorKind::Ring => false,
ArmorKind::Neck => false,
ArmorKind::Head => true,
ArmorKind::Tabard => false,
ArmorKind::Bag => false,
}
}
}
impl Armor {
/// Determines whether two pieces of armour are superficially equivalent to
/// one another (i.e: one may be substituted for the other in crafting
@ -212,8 +231,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 +260,8 @@ impl Armor {
set_stats * multiplier
},
}
};
base_stats * durability_multiplier.0
}
#[cfg(test)]

View File

@ -5,11 +5,11 @@ pub mod tool;
// Reexports
pub use modular::{MaterialStatManifest, ModularBase, ModularComponent};
pub use tool::{AbilitySet, AbilitySpec, Hands, Tool, ToolKind};
pub use tool::{AbilityMap, AbilitySet, AbilitySpec, Hands, Tool, ToolKind};
use crate::{
assets::{self, AssetExt, BoxedError, Error},
comp::inventory::{item::tool::AbilityMap, InvSlot},
comp::inventory::InvSlot,
effect::Effect,
recipe::RecipeInput,
terrain::Block,
@ -348,6 +348,21 @@ impl ItemKind {
};
result
}
pub fn has_durability(&self) -> bool {
match self {
ItemKind::Tool(_) => true,
ItemKind::Armor(armor) => armor.kind.has_durability(),
ItemKind::ModularComponent(_)
| ItemKind::Lantern(_)
| ItemKind::Glider
| ItemKind::Consumable { .. }
| ItemKind::Throwable { .. }
| ItemKind::Utility { .. }
| ItemKind::Ingredient { .. }
| ItemKind::TagExamples { .. } => false,
}
}
}
pub type ItemId = AtomicCell<Option<NonZeroU64>>;
@ -396,6 +411,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_lost: Option<u32>,
}
use std::hash::{Hash, Hasher};
@ -609,7 +628,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 +639,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.",
@ -766,6 +787,8 @@ impl assets::Asset for RawItemDef {
pub struct OperationFailure;
impl Item {
pub const MAX_DURABILITY: u32 = 8;
// TODO: consider alternatives such as default abilities that can be added to a
// loadout when no weapon is present
pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") }
@ -785,7 +808,9 @@ impl Item {
// These fields are updated immediately below
item_config: None,
hash: 0,
durability_lost: None,
};
item.durability_lost = item.has_durability().then_some(0);
item.update_item_state(ability_map, msm);
item
}
@ -1089,7 +1114,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 +1213,59 @@ impl Item {
}
}
pub fn durability(&self) -> Option<u32> {
self.durability_lost.map(|x| x.min(Self::MAX_DURABILITY))
}
pub fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
let durability_lost = self.durability_lost.unwrap_or(0);
debug_assert!(durability_lost <= Self::MAX_DURABILITY);
const DURABILITY_THRESHOLD: u32 = 4;
const MIN_FRAC: f32 = 0.2;
let mult = (1.0
- durability_lost.saturating_sub(DURABILITY_THRESHOLD) as f32
/ (Self::MAX_DURABILITY - DURABILITY_THRESHOLD) as f32)
* (1.0 - MIN_FRAC)
+ MIN_FRAC;
DurabilityMultiplier(mult)
}
pub fn has_durability(&self) -> bool { self.kind().has_durability() }
pub fn increment_damage(&mut self, ability_map: &AbilityMap, msm: &MaterialStatManifest) {
if let Some(durability_lost) = &mut self.durability_lost {
if *durability_lost < Self::MAX_DURABILITY {
*durability_lost += 1;
}
}
// Update item state after applying durability because stats have potential to
// change from different durability
self.update_item_state(ability_map, msm);
}
pub fn persistence_durability(&self) -> Option<NonZeroU32> {
self.durability_lost.and_then(NonZeroU32::new)
}
pub fn persistence_set_durability(&mut self, value: Option<NonZeroU32>) {
// If changes have been made so that item no longer needs to track durability,
// set to None
if !self.has_durability() {
self.durability_lost = None;
} else {
// Set durability to persisted value, and if item previously had no durability,
// set to Some(0) so that durability will be tracked
self.durability_lost = Some(value.map_or(0, NonZeroU32::get));
}
}
pub fn reset_durability(&mut self, ability_map: &AbilityMap, msm: &MaterialStatManifest) {
self.durability_lost = self.has_durability().then_some(0);
// Update item state after applying durability because stats have potential to
// change from different durability
self.update_item_state(ability_map, msm);
}
#[cfg(test)]
pub fn create_test_item_from_kind(kind: ItemKind) -> Self {
let ability_map = &AbilityMap::load().read();
@ -1211,10 +1289,11 @@ 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 has_durability(&self) -> bool;
fn durability(&self) -> Option<u32>;
fn stats_durability_multiplier(&self) -> DurabilityMultiplier;
fn tool_info(&self) -> Option<ToolKind> {
if let ItemKind::Tool(tool) = &*self.kind() {
@ -1243,6 +1322,14 @@ impl ItemDesc for Item {
fn is_modular(&self) -> bool { self.is_modular() }
fn components(&self) -> &[Item] { self.components() }
fn has_durability(&self) -> bool { self.has_durability() }
fn durability(&self) -> Option<u32> { self.durability() }
fn stats_durability_multiplier(&self) -> DurabilityMultiplier {
self.stats_durability_multiplier()
}
}
impl ItemDesc for ItemDef {
@ -1265,6 +1352,12 @@ impl ItemDesc for ItemDef {
fn is_modular(&self) -> bool { false }
fn components(&self) -> &[Item] { &[] }
fn has_durability(&self) -> bool { self.kind().has_durability() }
fn durability(&self) -> Option<u32> { None }
fn stats_durability_multiplier(&self) -> DurabilityMultiplier { DurabilityMultiplier(1.0) }
}
impl Component for Item {
@ -1278,6 +1371,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 +1392,14 @@ impl<'a, T: ItemDesc + ?Sized> ItemDesc for &'a T {
fn is_modular(&self) -> bool { (*self).is_modular() }
fn components(&self) -> &[Item] { (*self).components() }
fn has_durability(&self) -> bool { (*self).has_durability() }
fn durability(&self) -> Option<u32> { (*self).durability() }
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 {
@ -147,6 +146,20 @@ impl Stats {
let diminished = (self.buff_strength - base + 1.0).log(5.0);
base + diminished
}
pub fn with_durability_mult(&self, dur_mult: DurabilityMultiplier) -> Self {
let less_scaled = dur_mult.0 * 0.5 + 0.5;
Self {
equip_time_secs: self.equip_time_secs / less_scaled.max(0.01),
power: self.power * dur_mult.0,
effect_power: self.effect_power * dur_mult.0,
speed: self.speed * less_scaled,
crit_chance: self.crit_chance * dur_mult.0,
range: self.range * dur_mult.0,
energy_efficiency: self.energy_efficiency * dur_mult.0,
buff_strength: self.buff_strength * dur_mult.0,
}
}
}
impl Asset for Stats {
@ -171,9 +184,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 +205,7 @@ impl Sub<Stats> for Stats {
}
}
}
impl Mul<Stats> for Stats {
type Output = Self;
@ -206,9 +222,11 @@ impl Mul<Stats> for Stats {
}
}
}
impl MulAssign<Stats> for Stats {
fn mul_assign(&mut self, other: Stats) { *self = *self * other; }
}
impl Div<f32> for Stats {
type Output = Self;
@ -225,15 +243,18 @@ impl Div<f32> for Stats {
}
}
}
impl DivAssign<usize> for Stats {
fn div_assign(&mut self, scalar: usize) { *self = *self / (scalar as f32); }
impl Mul<DurabilityMultiplier> for Stats {
type Output = Self;
fn mul(self, value: DurabilityMultiplier) -> Self { self.with_durability_mult(value) }
}
#[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 +280,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 +382,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

@ -311,6 +311,12 @@ impl Loadout {
self.slots.iter().filter_map(|x| x.slot.as_ref())
}
pub(super) fn items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
self.slots
.iter()
.filter_map(|x| x.slot.as_ref().map(|i| (x.equip_slot, i)))
}
/// Checks that a slot can hold a given item
pub(super) fn slot_can_hold(
&self,
@ -416,6 +422,36 @@ impl Loadout {
}
});
}
/// Increments durability by 1 of all valid items
pub(super) fn damage_items(
&mut self,
ability_map: &item::tool::AbilityMap,
msm: &item::MaterialStatManifest,
) {
self.slots
.iter_mut()
.filter_map(|slot| slot.slot.as_mut())
.filter(|item| item.has_durability())
.for_each(|item| item.increment_damage(ability_map, msm));
}
/// Resets durability of item in specified slot
pub(super) fn repair_item_at_slot(
&mut self,
equip_slot: EquipSlot,
ability_map: &item::tool::AbilityMap,
msm: &item::MaterialStatManifest,
) {
if let Some(item) = self
.slots
.iter_mut()
.find(|slot| slot.equip_slot == equip_slot)
.and_then(|slot| slot.slot.as_mut())
{
item.reset_durability(ability_map, msm);
}
}
}
#[cfg(test)]

View File

@ -605,6 +605,10 @@ impl Inventory {
pub fn equipped_items(&self) -> impl Iterator<Item = &Item> { self.loadout.items() }
pub fn equipped_items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
self.loadout.items_with_slot()
}
/// Replaces the loadout item (if any) in the given EquipSlot with the
/// provided item, returning the item that was previously in the slot.
pub fn replace_loadout_item(
@ -878,6 +882,35 @@ impl Inventory {
}
});
}
/// Increments durability of all valid items equipped in loaodut by 1
pub fn damage_items(
&mut self,
ability_map: &item::tool::AbilityMap,
msm: &item::MaterialStatManifest,
) {
self.loadout.damage_items(ability_map, msm)
}
/// Resets durability of item in specified slot
pub fn repair_item_at_slot(
&mut self,
slot: Slot,
ability_map: &item::tool::AbilityMap,
msm: &item::MaterialStatManifest,
) {
match slot {
Slot::Inventory(invslot) => {
if let Some(Some(item)) = self.slot_mut(invslot) {
item.reset_durability(ability_map, msm);
}
},
Slot::Equip(equip_slot) => {
self.loadout
.repair_item_at_slot(equip_slot, ability_map, msm);
},
}
}
}
impl Component for Inventory {

View File

@ -1022,16 +1022,17 @@ impl TradePricing {
#[cfg(test)]
fn print_sorted(&self) {
use crate::comp::item::armor; //, ItemKind, MaterialStatManifest};
use crate::comp::item::{armor, DurabilityMultiplier}; //, ItemKind, MaterialStatManifest};
println!("Item, ForSale, Amount, Good, Quality, Deal, Unit,");
fn more_information(i: &Item, p: f32) -> (String, &'static str) {
let msm = &MaterialStatManifest::load().read();
let durability_multiplier = DurabilityMultiplier(1.0);
if let ItemKind::Armor(a) = &*i.kind() {
(
match a.stats(msm).protection {
match a.stats(msm, durability_multiplier).protection {
Some(armor::Protection::Invincible) => "Invincible".into(),
Some(armor::Protection::Normal(x)) => format!("{:.4}", x * p),
None => "0.0".into(),
@ -1039,10 +1040,8 @@ impl TradePricing {
"prot/val",
)
} else if let ItemKind::Tool(t) = &*i.kind() {
(
format!("{:.4}", t.stats.power * t.stats.speed * p),
"dps/val",
)
let stats = t.stats(durability_multiplier);
(format!("{:.4}", stats.power * stats.speed * p), "dps/val")
} else if let ItemKind::Consumable { kind: _, effects } = &*i.kind() {
(
effects

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

@ -11,7 +11,8 @@
trait_alias,
type_alias_impl_trait,
extend_one,
arbitrary_self_types
arbitrary_self_types,
int_roundings
)]
#![feature(hash_drain_filter)]

View File

@ -1,11 +1,12 @@
use crate::{
assets::{self, AssetExt, AssetHandle},
comp::{
inventory::slot::InvSlotId,
inventory::slot::{InvSlotId, Slot},
item::{
modular,
tool::{AbilityMap, ToolKind},
ItemBase, ItemDef, ItemDefinitionIdOwned, ItemKind, ItemTag, MaterialStatManifest,
ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, ItemTag,
MaterialStatManifest,
},
Inventory, Item,
},
@ -39,6 +40,45 @@ pub enum RecipeInput {
ListSameItem(Vec<Arc<ItemDef>>),
}
impl RecipeInput {
fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
&'a self,
amount: u32,
slot_claims: &mut HashMap<InvSlotId, u32>,
unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
inv: &Inventory,
input_slots: I,
) {
let mut required = amount;
// contains_any check used for recipes that have an input that is not consumed,
// e.g. craftsman hammer
// Goes through each slot and marks some amount from each slot as claimed
let contains_any = input_slots.into_iter().all(|slot| {
// Checks that the item in the slot can be used for the input
if let Some(item) = inv
.get(slot)
.filter(|item| item.matches_recipe_input(self, amount))
{
// Gets the number of items claimed from the slot, or sets to 0 if slot has
// not been claimed by another input yet
let claimed = slot_claims.entry(slot).or_insert(0);
let available = item.amount().saturating_sub(*claimed);
let provided = available.min(required);
required -= provided;
*claimed += provided;
true
} else {
false
}
});
// If there were not sufficient items to cover requirement between all provided
// slots, or if non-consumed item was not present, mark input as not satisfied
if required > 0 || !contains_any {
unsatisfied_requirements.push((self, required));
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Recipe {
pub output: (Arc<ItemDef>, u32),
@ -477,13 +517,11 @@ impl assets::Compound for RecipeBook {
cache: assets::AnyCache,
specifier: &assets::SharedString,
) -> Result<Self, assets::BoxedError> {
#[inline]
fn load_item_def(spec: &(String, u32)) -> Result<(Arc<ItemDef>, u32), assets::Error> {
let def = Arc::<ItemDef>::load_cloned(&spec.0)?;
Ok((def, spec.1))
}
#[inline]
fn load_recipe_input(
(input, amount, is_mod_comp): &(RawRecipeInput, u32, bool),
) -> Result<(RecipeInput, u32, bool), assets::Error> {
@ -594,65 +632,26 @@ impl ComponentRecipe {
let mut slot_claims = HashMap::new();
let mut unsatisfied_requirements = Vec::new();
fn handle_requirement<'a, I: Iterator<Item = InvSlotId>>(
slot_claims: &mut HashMap<InvSlotId, u32>,
unsatisfied_requirements: &mut Vec<(&'a RecipeInput, u32)>,
inv: &Inventory,
input: &'a RecipeInput,
amount: u32,
input_slots: I,
) {
let mut required = amount;
// contains_any check used for recipes that have an input that is not consumed,
// e.g. craftsman hammer
// Goes through each slot and marks some amount from each slot as claimed
let contains_any = input_slots.into_iter().all(|slot| {
// Checks that the item in the slot can be used for the input
if let Some(item) = inv
.get(slot)
.filter(|item| item.matches_recipe_input(input, amount))
{
// Gets the number of items claimed from the slot, or sets to 0 if slot has
// not been claimed by another input yet
let claimed = slot_claims.entry(slot).or_insert(0);
let available = item.amount().saturating_sub(*claimed);
let provided = available.min(required);
required -= provided;
*claimed += provided;
true
} else {
false
}
});
// If there were not sufficient items to cover requirement between all provided
// slots, or if non-consumed item was not present, mark input as not satisfied
if required > 0 || !contains_any {
unsatisfied_requirements.push((input, required));
}
}
// Checks each input against slots in the inventory. If the slots contain an
// item that fulfills the need of the input, marks some of the item as claimed
// up to quantity needed for the crafting input. If the item either
// cannot be used, or there is insufficient quantity, adds input and
// number of materials needed to unsatisfied requirements.
handle_requirement(
self.material.0.handle_requirement(
self.material.1,
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
&self.material.0,
self.material.1,
core::iter::once(material_slot),
);
if let Some((modifier_input, modifier_amount)) = &self.modifier {
// TODO: Better way to get slot to use that ensures this requirement fails if no
// slot provided?
handle_requirement(
modifier_input.handle_requirement(
*modifier_amount,
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
modifier_input,
*modifier_amount,
core::iter::once(modifier_slot.unwrap_or(InvSlotId::new(0, 0))),
);
}
@ -666,12 +665,11 @@ impl ComponentRecipe {
.filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
.copied();
// Checks if requirement is met, and if not marks it as unsatisfied
handle_requirement(
input.handle_requirement(
*amount,
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
input,
*amount,
input_slots,
);
});
@ -835,7 +833,6 @@ impl assets::Compound for ComponentRecipeBook {
cache: assets::AnyCache,
specifier: &assets::SharedString,
) -> Result<Self, assets::BoxedError> {
#[inline]
fn create_recipe_key(raw_recipe: &RawComponentRecipe) -> ComponentKey {
match &raw_recipe.output {
RawComponentOutput::ToolPrimaryComponent { toolkind, item: _ } => {
@ -853,7 +850,6 @@ impl assets::Compound for ComponentRecipeBook {
}
}
#[inline]
fn load_recipe(raw_recipe: &RawComponentRecipe) -> Result<ComponentRecipe, assets::Error> {
let output = match &raw_recipe.output {
RawComponentOutput::ToolPrimaryComponent { toolkind: _, item } => {
@ -900,6 +896,210 @@ impl assets::Compound for ComponentRecipeBook {
}
}
#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Debug)]
enum RepairKey {
ItemDefId(String),
ModularWeapon { material: String },
}
impl RepairKey {
fn from_item(item: &Item) -> Option<Self> {
match item.item_definition_id() {
ItemDefinitionId::Simple(item_id) => Some(Self::ItemDefId(String::from(item_id))),
ItemDefinitionId::Compound { .. } => None,
ItemDefinitionId::Modular { pseudo_base, .. } => match pseudo_base {
"veloren.core.pseudo_items.modular.tool" => {
if let Some(ItemDefinitionId::Simple(material)) = item
.components()
.iter()
.find(|comp| {
matches!(
&*comp.kind(),
ItemKind::ModularComponent(
modular::ModularComponent::ToolPrimaryComponent { .. }
)
)
})
.and_then(|comp| {
comp.components()
.iter()
.next()
.map(|comp| comp.item_definition_id())
})
{
let material = String::from(material);
Some(Self::ModularWeapon { material })
} else {
None
}
},
_ => None,
},
}
}
}
#[derive(Serialize, Deserialize, Clone)]
struct RawRepairRecipe {
inputs: Vec<(RawRecipeInput, u32)>,
}
#[derive(Serialize, Deserialize, Clone)]
struct RawRepairRecipeBook {
recipes: HashMap<RepairKey, RawRepairRecipe>,
fallback: RawRepairRecipe,
}
impl assets::Asset for RawRepairRecipeBook {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RepairRecipe {
inputs: Vec<(RecipeInput, u32)>,
}
impl RepairRecipe {
/// Determine whether the inventory contains the ingredients for a repair.
/// If it does, return a vec of inventory slots that contain the
/// ingredients needed, whose positions correspond to particular repair
/// inputs. If items are missing, return the missing items, and how many
/// are missing.
pub fn inventory_contains_ingredients(
&self,
item: &Item,
inv: &Inventory,
) -> Result<Vec<(u32, InvSlotId)>, Vec<(&RecipeInput, u32)>> {
inventory_contains_ingredients(self.inputs(item), inv, 1)
}
pub fn inputs(&self, item: &Item) -> impl Iterator<Item = (&RecipeInput, u32)> {
let item_durability = item.durability().unwrap_or(0);
self.inputs
.iter()
.filter_map(move |(input, original_amount)| {
let amount = (original_amount * item_durability) / Item::MAX_DURABILITY;
// If original repair recipe consumed ingredients, but item not damaged enough
// to actually need to consume item, remove item as requirement.
if *original_amount > 0 && amount == 0 {
None
} else {
Some((input, amount))
}
})
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RepairRecipeBook {
recipes: HashMap<RepairKey, RepairRecipe>,
fallback: RepairRecipe,
}
impl RepairRecipeBook {
pub fn repair_recipe(&self, item: &Item) -> Option<&RepairRecipe> {
RepairKey::from_item(item)
.as_ref()
.and_then(|key| self.recipes.get(key))
.or_else(|| item.has_durability().then_some(&self.fallback))
}
pub fn repair_item(
&self,
inv: &mut Inventory,
item: Slot,
slots: Vec<(u32, InvSlotId)>,
ability_map: &AbilityMap,
msm: &MaterialStatManifest,
) -> Result<(), Vec<(&RecipeInput, u32)>> {
let mut slot_claims = HashMap::new();
let mut unsatisfied_requirements = Vec::new();
if let Some(item) = match item {
Slot::Equip(slot) => inv.equipped(slot),
Slot::Inventory(slot) => inv.get(slot),
} {
if let Some(repair_recipe) = self.repair_recipe(item) {
repair_recipe
.inputs(item)
.enumerate()
.for_each(|(i, (input, amount))| {
// Gets all slots provided for this input by the frontend
let input_slots = slots
.iter()
.filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
.copied();
// Checks if requirement is met, and if not marks it as unsatisfied
input.handle_requirement(
amount,
&mut slot_claims,
&mut unsatisfied_requirements,
inv,
input_slots,
);
})
}
}
if unsatisfied_requirements.is_empty() {
for (slot, to_remove) in slot_claims.iter() {
for _ in 0..*to_remove {
let _ = inv
.take(*slot, ability_map, msm)
.expect("Expected item to exist in the inventory");
}
}
inv.repair_item_at_slot(item, ability_map, msm);
Ok(())
} else {
Err(unsatisfied_requirements)
}
}
}
impl assets::Compound for RepairRecipeBook {
fn load(
cache: assets::AnyCache,
specifier: &assets::SharedString,
) -> Result<Self, assets::BoxedError> {
fn load_recipe_input(
(input, amount): &(RawRecipeInput, u32),
) -> Result<(RecipeInput, u32), assets::Error> {
let input = input.load_recipe_input()?;
Ok((input, *amount))
}
let raw = cache.load::<RawRepairRecipeBook>(specifier)?.cloned();
let recipes = raw
.recipes
.iter()
.map(|(key, RawRepairRecipe { inputs })| {
let inputs = inputs
.iter()
.map(load_recipe_input)
.collect::<Result<Vec<_>, _>>()?;
Ok((key.clone(), RepairRecipe { inputs }))
})
.collect::<Result<_, assets::Error>>()?;
let fallback = RepairRecipe {
inputs: raw
.fallback
.inputs
.iter()
.map(load_recipe_input)
.collect::<Result<Vec<_>, _>>()?,
};
Ok(RepairRecipeBook { recipes, fallback })
}
}
pub fn default_recipe_book() -> AssetHandle<RecipeBook> {
RecipeBook::load_expect("common.recipe_book")
}
@ -908,6 +1108,10 @@ pub fn default_component_recipe_book() -> AssetHandle<ComponentRecipeBook> {
ComponentRecipeBook::load_expect("common.component_recipe_book")
}
pub fn default_repair_recipe_book() -> AssetHandle<RepairRecipeBook> {
RepairRecipeBook::load_expect("common.repair_recipe_book")
}
impl assets::Compound for ReverseComponentRecipeBook {
fn load(
cache: assets::AnyCache,

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

@ -291,6 +291,7 @@ impl Block {
| SpriteKind::Loom
| SpriteKind::SpinningWheel
| SpriteKind::DismantlingBench
| SpriteKind::RepairBench
| SpriteKind::TanningRack
| SpriteKind::Chest
| SpriteKind::DungeonChest0

View File

@ -240,6 +240,7 @@ make_case_elim!(
Keyhole = 0xD7,
KeyDoor = 0xD8,
CommonLockedChest = 0xD9,
RepairBench = 0xDA,
}
);
@ -308,6 +309,7 @@ impl SpriteKind {
SpriteKind::CookingPot => 1.36,
SpriteKind::DismantlingBench => 1.18,
SpriteKind::IceSpike => 1.0,
SpriteKind::RepairBench => 1.2,
// TODO: Find suitable heights.
SpriteKind::BarrelCactus
| SpriteKind::RoundCactus
@ -581,6 +583,7 @@ impl SpriteKind {
| SpriteKind::TanningRack
| SpriteKind::Loom
| SpriteKind::DismantlingBench
| SpriteKind::RepairBench
| SpriteKind::ChristmasOrnament
| SpriteKind::ChristmasWreath
| SpriteKind::WindowArabic

View File

@ -18,7 +18,7 @@ use common::{
comp::{
self, aura, buff,
chat::{KillSource, KillType},
inventory::item::MaterialStatManifest,
inventory::item::{AbilityMap, MaterialStatManifest},
loot_owner::LootOwnerKind,
Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory,
Player, Poise, Pos, SkillSet, Stats,
@ -511,6 +511,13 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
true
};
// Modify durability on all equipped items
if let Some(mut inventory) = state.ecs().write_storage::<Inventory>().get_mut(entity) {
let ability_map = state.ecs().read_resource::<AbilityMap>();
let msm = state.ecs().read_resource::<MaterialStatManifest>();
inventory.damage_items(&ability_map, &msm);
}
if should_delete {
if let Some(rtsim_entity) = state
.ecs()

View File

@ -12,7 +12,9 @@ use common::{
slot::{self, Slot},
},
consts::MAX_PICKUP_RANGE,
recipe::{self, default_component_recipe_book, default_recipe_book},
recipe::{
self, default_component_recipe_book, default_recipe_book, default_repair_recipe_book,
},
terrain::{Block, SpriteKind},
trade::Trades,
uid::Uid,
@ -856,6 +858,20 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
None
}
},
CraftEvent::Repair { item, slots } => {
let repair_recipes = default_repair_recipe_book().read();
let sprite = get_craft_sprite(state, craft_sprite);
if matches!(sprite, Some(SpriteKind::RepairBench)) {
let _ = repair_recipes.repair_item(
&mut inventory,
item,
slots,
&state.ecs().read_resource::<AbilityMap>(),
&state.ecs().read_resource::<item::MaterialStatManifest>(),
);
}
None
},
};
// Attempt to insert items into inventory, dropping them if there is not enough

View File

@ -0,0 +1 @@
ALTER TABLE item ADD properties TEXT NOT NULL DEFAULT '{}';

View File

@ -70,13 +70,15 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
parent_container_item_id,
item_definition_id,
stack_size,
position
position,
properties
) AS (
SELECT item_id,
parent_container_item_id,
item_definition_id,
stack_size,
position
position,
properties
FROM item
WHERE parent_container_item_id = ?1
UNION ALL
@ -84,7 +86,8 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
item.parent_container_item_id,
item.item_definition_id,
item.stack_size,
item.position
item.position,
item.properties
FROM item, items_tree
WHERE item.parent_container_item_id = items_tree.item_id
)
@ -100,6 +103,7 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
item_definition_id: row.get(2)?,
stack_size: row.get(3)?,
position: row.get(4)?,
properties: row.get(5)?,
})
})?
.filter_map(Result::ok)
@ -390,6 +394,7 @@ pub fn create_character(
parent_container_item_id: WORLD_PSEUDO_CONTAINER_ID,
item_definition_id: CHARACTER_PSEUDO_CONTAINER_DEF_ID.to_owned(),
position: character_id.to_string(),
properties: String::new(),
},
Item {
stack_size: 1,
@ -397,6 +402,7 @@ pub fn create_character(
parent_container_item_id: character_id,
item_definition_id: INVENTORY_PSEUDO_CONTAINER_DEF_ID.to_owned(),
position: INVENTORY_PSEUDO_CONTAINER_POSITION.to_owned(),
properties: String::new(),
},
Item {
stack_size: 1,
@ -404,6 +410,7 @@ pub fn create_character(
parent_container_item_id: character_id,
item_definition_id: LOADOUT_PSEUDO_CONTAINER_DEF_ID.to_owned(),
position: LOADOUT_PSEUDO_CONTAINER_POSITION.to_owned(),
properties: String::new(),
},
];
@ -413,8 +420,9 @@ pub fn create_character(
parent_container_item_id,
item_definition_id,
stack_size,
position)
VALUES (?1, ?2, ?3, ?4, ?5)",
position,
properties)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
for pseudo_container in pseudo_containers {
@ -424,6 +432,7 @@ pub fn create_character(
&pseudo_container.item_definition_id,
&pseudo_container.stack_size,
&pseudo_container.position,
&pseudo_container.properties,
])?;
}
drop(stmt);
@ -521,8 +530,9 @@ pub fn create_character(
parent_container_item_id,
item_definition_id,
stack_size,
position)
VALUES (?1, ?2, ?3, ?4, ?5)",
position,
properties)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
for item in inserts {
@ -532,6 +542,7 @@ pub fn create_character(
&item.model.item_definition_id,
&item.model.stack_size,
&item.model.position,
&item.model.properties,
])?;
}
drop(stmt);
@ -1048,8 +1059,9 @@ pub fn update(
parent_container_item_id,
item_definition_id,
stack_size,
position)
VALUES (?1, ?2, ?3, ?4, ?5)",
position,
properties)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
for item in upserted_items.iter() {
@ -1059,6 +1071,7 @@ pub fn update(
&item.item_definition_id,
&item.stack_size,
&item.position,
&item.properties,
])?;
}
}

View File

@ -5,7 +5,10 @@ use crate::persistence::{
use crate::persistence::{
error::PersistenceError,
json_models::{self, CharacterPosition, DatabaseAbilitySet, GenericBody, HumanoidBody},
json_models::{
self, CharacterPosition, DatabaseAbilitySet, DatabaseItemProperties, GenericBody,
HumanoidBody,
},
};
use common::{
character::CharacterId,
@ -165,6 +168,8 @@ pub fn convert_items_to_database_items(
bfs_queue.push_back((format!("component_{}", i), Some(component), item_id));
}
let item_properties = json_models::item_properties_to_db_model(item);
let upsert = ItemModelPair {
model: Item {
item_definition_id: String::from(item.persistence_item_id()),
@ -176,6 +181,8 @@ pub fn convert_items_to_database_items(
} else {
1
},
properties: serde_json::to_string(&item_properties)
.expect("Failed to convert item properties to a json string."),
},
// Continue to remember the atomic, in case we detect an error later and want
// to roll back to preserve liveness.
@ -359,6 +366,9 @@ pub fn convert_inventory_from_database_items(
item_indices.insert(db_item.item_id, i);
let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
let item_properties =
serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
json_models::apply_db_item_properties(&mut item, &item_properties);
// NOTE: Since this is freshly loaded, the atomic is *unique.*
let comp = item.get_item_id_for_database();
@ -459,7 +469,10 @@ pub fn convert_loadout_from_database_items(
for (i, db_item) in database_items.iter().enumerate() {
item_indices.insert(db_item.item_id, i);
let item = get_item_from_asset(db_item.item_definition_id.as_str())?;
let mut item = get_item_from_asset(db_item.item_definition_id.as_str())?;
let item_properties =
serde_json::de::from_str::<DatabaseItemProperties>(&db_item.properties)?;
json_models::apply_db_item_properties(&mut item, &item_properties);
// NOTE: item id is currently *unique*, so we can store the ID safely.
let comp = item.get_item_id_for_database();

View File

@ -2,7 +2,7 @@ use common::comp;
use common_base::dev_panic;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::string::ToString;
use std::{num::NonZeroU32, string::ToString};
use vek::{Vec2, Vec3};
#[derive(Serialize, Deserialize)]
@ -285,3 +285,36 @@ pub fn active_abilities_from_db_model(
.collect::<HashMap<_, _>>();
comp::ability::ActiveAbilities::new(ability_sets)
}
/// Struct containing item properties in the format that they get persisted to
/// the database. Adding new fields is generally safe as long as they are
/// optional. Renaming or removing old fields will require a migration.
#[derive(Serialize, Deserialize)]
pub struct DatabaseItemProperties {
#[serde(skip_serializing_if = "Option::is_none")]
durability: Option<NonZeroU32>,
}
pub fn item_properties_to_db_model(item: &comp::Item) -> DatabaseItemProperties {
DatabaseItemProperties {
durability: item.persistence_durability(),
}
}
pub fn apply_db_item_properties(item: &mut comp::Item, properties: &DatabaseItemProperties) {
let DatabaseItemProperties { durability } = properties;
item.persistence_set_durability(*durability);
}
#[cfg(test)]
pub mod tests {
#[test]
fn test_default_item_properties() {
use super::DatabaseItemProperties;
const DEFAULT_ITEM_PROPERTIES: &str = "{}";
let _ = serde_json::de::from_str::<DatabaseItemProperties>(DEFAULT_ITEM_PROPERTIES).expect(
"Default value should always load to ensure that changes to item properties is always \
forward compatible with migration V50.",
);
}
}

View File

@ -12,6 +12,7 @@ pub struct Item {
pub item_definition_id: String,
pub stack_size: i32,
pub position: String,
pub properties: String,
}
pub struct Body {

View File

@ -8,7 +8,7 @@ use crate::{
use common::{
comp::{self, Admin, Player, Stats},
event::{EventBus, ServerEvent},
recipe::{default_component_recipe_book, default_recipe_book},
recipe::{default_component_recipe_book, default_recipe_book, default_repair_recipe_book},
resources::TimeOfDay,
shared_server_config::ServerConstants,
uid::{Uid, UidAllocator},
@ -348,6 +348,7 @@ impl<'a> System<'a> for Sys {
world_map: (*read_data.map).clone(),
recipe_book: default_recipe_book().cloned(),
component_recipe_book: default_component_recipe_book().cloned(),
repair_recipe_book: default_repair_recipe_book().cloned(),
material_stats: (*read_data.material_stats).clone(),
ability_map: (*read_data.ability_map).clone(),
server_constants: ServerConstants {

View File

@ -22,7 +22,7 @@ use common::{
Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, MaterialStatManifest, Quality,
TagExampleInfo,
},
slot::InvSlotId,
slot::{InvSlotId, Slot},
Inventory,
},
recipe::{ComponentKey, Recipe, RecipeInput},
@ -86,8 +86,8 @@ widget_ids! {
dismantle_title,
dismantle_img,
dismantle_txt,
dismantle_highlight_txt,
modular_inputs[],
repair_buttons[],
craft_slots[],
modular_art,
modular_desc_txt,
modular_wep_empty_bg,
@ -115,6 +115,9 @@ pub enum Event {
Focus(widget::Id),
SearchRecipe(Option<String>),
ClearRecipeInputs,
RepairItem {
slot: Slot,
},
}
pub struct CraftingShow {
@ -122,8 +125,9 @@ pub struct CraftingShow {
pub crafting_search_key: Option<String>,
pub craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
pub salvage: bool,
pub initialize_repair: bool,
// TODO: Maybe try to do something that doesn't need to allocate?
pub recipe_inputs: HashMap<u32, InvSlotId>,
pub recipe_inputs: HashMap<u32, Slot>,
}
impl Default for CraftingShow {
@ -133,6 +137,7 @@ impl Default for CraftingShow {
crafting_search_key: None,
craft_sprite: None,
salvage: false,
initialize_repair: false,
recipe_inputs: HashMap::new(),
}
}
@ -207,7 +212,7 @@ pub enum CraftingTab {
Bag,
Utility,
Glider,
Dismantle, // Needs to be the last one or widget alignment will be messed up
Dismantle,
}
impl CraftingTab {
@ -239,7 +244,8 @@ impl CraftingTab {
CraftingTab::Weapon => imgs.icon_weapon,
CraftingTab::Bag => imgs.icon_bag,
CraftingTab::ProcessedMaterial => imgs.icon_processed_material,
CraftingTab::Dismantle => imgs.icon_dismantle,
// These tabs are never shown, so using not found is fine
CraftingTab::Dismantle => imgs.not_found,
}
}
@ -271,6 +277,10 @@ impl CraftingTab {
},
}
}
// Tells UI whether tab is an adhoc tab that should only sometimes be present
// depending on what station is accessed
fn is_adhoc(self) -> bool { matches!(self, CraftingTab::Dismantle) }
}
pub struct State {
@ -313,6 +323,16 @@ impl<'a> Widget for Crafting<'a> {
let mut events = Vec::new();
// Handle any initialization
// TODO: Replace with struct instead of making assorted booleans once there is
// more than 1 field.
if self.show.crafting_fields.initialize_repair {
state.update(|s| {
s.selected_recipe = Some(String::from("veloren.core.pseudo_recipe.repair"))
});
}
self.show.crafting_fields.initialize_repair = false;
// Tooltips
let item_tooltip = ItemTooltip::new(
{
@ -434,56 +454,57 @@ impl<'a> Widget for Crafting<'a> {
})
};
let sel_crafting_tab = &self.show.crafting_fields.crafting_tab;
for (i, crafting_tab) in CraftingTab::iter().enumerate() {
if crafting_tab != CraftingTab::Dismantle {
let tab_img = crafting_tab.img_id(self.imgs);
// Button Background
let mut bg = Image::new(self.imgs.pixel)
.w_h(40.0, 30.0)
.color(Some(UI_MAIN));
if i == 0 {
bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0)
} else {
bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0)
};
bg.set(state.ids.category_bgs[i], ui);
// Category Button
if Button::image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_pressed
} else {
self.imgs.wpn_icon_border
})
.wh_of(state.ids.category_bgs[i])
.middle_of(state.ids.category_bgs[i])
.hover_image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_pressed
} else {
self.imgs.wpn_icon_border_mo
})
.press_image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_pressed
} else {
self.imgs.wpn_icon_border_press
})
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get_msg(crafting_tab.name_key()),
"",
&tabs_tooltip,
TEXT_COLOR,
)
.set(state.ids.category_tabs[i], ui)
.was_clicked()
{
events.push(Event::ChangeCraftingTab(crafting_tab))
};
// Tab images
Image::new(tab_img)
.middle_of(state.ids.category_tabs[i])
.w_h(20.0, 20.0)
.graphics_for(state.ids.category_tabs[i])
.set(state.ids.category_imgs[i], ui);
}
for (i, crafting_tab) in CraftingTab::iter()
.filter(|tab| !tab.is_adhoc())
.enumerate()
{
let tab_img = crafting_tab.img_id(self.imgs);
// Button Background
let mut bg = Image::new(self.imgs.pixel)
.w_h(40.0, 30.0)
.color(Some(UI_MAIN));
if i == 0 {
bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0)
} else {
bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0)
};
bg.set(state.ids.category_bgs[i], ui);
// Category Button
if Button::image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_pressed
} else {
self.imgs.wpn_icon_border
})
.wh_of(state.ids.category_bgs[i])
.middle_of(state.ids.category_bgs[i])
.hover_image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_pressed
} else {
self.imgs.wpn_icon_border_mo
})
.press_image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_pressed
} else {
self.imgs.wpn_icon_border_press
})
.with_tooltip(
self.tooltip_manager,
&self.localized_strings.get_msg(crafting_tab.name_key()),
"",
&tabs_tooltip,
TEXT_COLOR,
)
.set(state.ids.category_tabs[i], ui)
.was_clicked()
{
events.push(Event::ChangeCraftingTab(crafting_tab))
};
// Tab images
Image::new(tab_img)
.middle_of(state.ids.category_tabs[i])
.w_h(20.0, 20.0)
.graphics_for(state.ids.category_tabs[i])
.set(state.ids.category_imgs[i], ui);
}
// TODO: Consider UX for filtering searches, maybe a checkbox or a dropdown if
@ -521,40 +542,45 @@ impl<'a> Widget for Crafting<'a> {
let weapon_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
let metal_comp_recipe = make_pseudo_recipe(SpriteKind::Anvil);
let wood_comp_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
let modular_entries = {
let repair_recipe = make_pseudo_recipe(SpriteKind::RepairBench);
let pseudo_entries = {
// A BTreeMap is used over a HashMap as when a HashMap is used, the UI shuffles
// the positions of these every tick, so a BTreeMap is necessary to keep it
// ordered.
let mut modular_entries = BTreeMap::new();
modular_entries.insert(
let mut pseudo_entries = BTreeMap::new();
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon"),
(&weapon_recipe, "Modular Weapon"),
(&weapon_recipe, "Modular Weapon", CraftingTab::Weapon),
);
modular_entries.insert(
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.sword"),
(&metal_comp_recipe, "Sword Blade"),
(&metal_comp_recipe, "Sword Blade", CraftingTab::Weapon),
);
modular_entries.insert(
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.axe"),
(&metal_comp_recipe, "Axe Head"),
(&metal_comp_recipe, "Axe Head", CraftingTab::Weapon),
);
modular_entries.insert(
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.hammer"),
(&metal_comp_recipe, "Hammer Head"),
(&metal_comp_recipe, "Hammer Head", CraftingTab::Weapon),
);
modular_entries.insert(
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.bow"),
(&wood_comp_recipe, "Bow Limbs"),
(&wood_comp_recipe, "Bow Limbs", CraftingTab::Weapon),
);
modular_entries.insert(
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.staff"),
(&wood_comp_recipe, "Staff Shaft"),
(&wood_comp_recipe, "Staff Shaft", CraftingTab::Weapon),
);
modular_entries.insert(
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon_component.sceptre"),
(&wood_comp_recipe, "Sceptre Shaft"),
(&wood_comp_recipe, "Sceptre Shaft", CraftingTab::Weapon),
);
modular_entries
pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.repair"),
(&repair_recipe, "Repair Equipment", CraftingTab::All),
);
pseudo_entries
};
// First available recipes, then ones with available materials,
@ -605,36 +631,34 @@ impl<'a> Widget for Crafting<'a> {
(name, recipe, is_craftable, has_materials)
})
.chain(
matches!(sel_crafting_tab, CraftingTab::Weapon | CraftingTab::All)
.then_some(
modular_entries
.iter()
.filter(|(_, (_, output_name))| {
match search_filter {
SearchFilter::None => {
let output_name = output_name.to_lowercase();
search_keys
.iter()
.all(|&substring| output_name.contains(substring))
},
// TODO: Get input filtering to work here, probably requires
// checking component recipe book?
SearchFilter::Input => false,
SearchFilter::Nonexistent => false,
}
})
.map(|(recipe_name, (recipe, _))| {
(
recipe_name,
*recipe,
self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
== recipe.craft_sprite,
true,
)
}),
)
.into_iter()
.flatten(),
pseudo_entries
.iter()
// Filter by selected tab
.filter(|(_, (_, _, tab))| *sel_crafting_tab == CraftingTab::All || sel_crafting_tab == tab)
// Filter by search filter
.filter(|(_, (_, output_name, _))| {
match search_filter {
SearchFilter::None => {
let output_name = output_name.to_lowercase();
search_keys
.iter()
.all(|&substring| output_name.contains(substring))
},
// TODO: Get input filtering to work here, probably requires
// checking component recipe book?
SearchFilter::Input => false,
SearchFilter::Nonexistent => false,
}
})
.map(|(recipe_name, (recipe, _, _))| {
(
recipe_name,
*recipe,
self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
== recipe.craft_sprite,
true,
)
}),
)
.collect();
ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials)| {
@ -647,7 +671,7 @@ impl<'a> Widget for Crafting<'a> {
});
// Recipe list
let recipe_list_length = self.client.recipe_book().iter().len() + modular_entries.len();
let recipe_list_length = self.client.recipe_book().iter().len() + pseudo_entries.len();
if state.ids.recipe_list_btns.len() < recipe_list_length {
state.update(|state| {
state
@ -702,11 +726,12 @@ impl<'a> Widget for Crafting<'a> {
.press_image(self.imgs.selection_press)
.image_color(color::rgba(1.0, 0.82, 0.27, 1.0));
let recipe_name = if let Some((_recipe, modular_name)) = modular_entries.get(name) {
*modular_name
} else {
&recipe.output.0.name
};
let recipe_name =
if let Some((_recipe, pseudo_name, _filter_tab)) = pseudo_entries.get(name) {
*pseudo_name
} else {
&recipe.output.0.name
};
let text = Text::new(recipe_name)
.color(if is_craftable {
@ -735,12 +760,9 @@ impl<'a> Widget for Crafting<'a> {
if state.selected_recipe.as_ref() == Some(name) {
state.update(|s| s.selected_recipe = None);
} else {
if matches!(
self.show.crafting_fields.crafting_tab,
CraftingTab::Dismantle
) {
// If current tab is dismantle, and recipe is selected, change to general
// tab, as in dismantle tab recipe gets deselected
if self.show.crafting_fields.crafting_tab.is_adhoc() {
// If current tab is an adhoc tab, and recipe is selected, change to general
// tab
events.push(Event::ChangeCraftingTab(CraftingTab::All));
}
state.update(|s| s.selected_recipe = Some(name.clone()));
@ -802,19 +824,17 @@ impl<'a> Widget for Crafting<'a> {
}
}
// Deselect recipe if current tab is dismantle, elsewhere if recipe selected
// while dismantling, tab is changed to general
if matches!(
self.show.crafting_fields.crafting_tab,
CraftingTab::Dismantle
) {
// Deselect recipe if current tab is an adhoc tab, elsewhere if recipe selected
// while in an adhoc tab, tab is changed to general
if self.show.crafting_fields.crafting_tab.is_adhoc() {
state.update(|s| s.selected_recipe = None);
}
// Selected Recipe
if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() {
Some(selected_recipe) => {
if let Some((modular_recipe, _modular_name)) = modular_entries.get(selected_recipe)
if let Some((modular_recipe, _pseudo_name, _filter_tab)) =
pseudo_entries.get(selected_recipe)
{
Some((selected_recipe, *modular_recipe))
} else {
@ -827,8 +847,10 @@ impl<'a> Widget for Crafting<'a> {
None => None,
} {
let recipe_name = String::from(recipe_name);
let title = if let Some((_recipe, modular_name)) = modular_entries.get(&recipe_name) {
*modular_name
let title = if let Some((_recipe, pseudo_name, _filter_tab)) =
pseudo_entries.get(&recipe_name)
{
*pseudo_name
} else {
&recipe.output.0.name
};
@ -846,6 +868,7 @@ impl<'a> Widget for Crafting<'a> {
ModularWeapon,
Component(ToolKind),
Simple,
Repair,
}
let recipe_kind = match recipe_name.as_str() {
@ -868,37 +891,36 @@ impl<'a> Widget for Crafting<'a> {
"veloren.core.pseudo_recipe.modular_weapon_component.sceptre" => {
RecipeKind::Component(ToolKind::Sceptre)
},
"veloren.core.pseudo_recipe.repair" => RecipeKind::Repair,
_ => RecipeKind::Simple,
};
// Output slot, tags, and modular input slots
let (modular_primary_slot, modular_secondary_slot, can_perform) = match recipe_kind {
RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
let mut slot_maker = SlotMaker {
empty_slot: self.imgs.inv_slot,
filled_slot: self.imgs.inv_slot,
selected_slot: self.imgs.inv_slot_sel,
background_color: Some(UI_MAIN),
content_size: ContentSize {
width_height_ratio: 1.0,
max_fraction: 0.75,
},
selected_content_scale: 1.067,
amount_font: self.fonts.cyri.conrod_id,
amount_margins: Vec2::new(-4.0, 0.0),
amount_font_size: self.fonts.cyri.scale(12),
amount_text_color: TEXT_COLOR,
content_source: self.inventory,
image_source: self.item_imgs,
slot_manager: Some(self.slot_manager),
pulse: self.pulse,
};
let mut slot_maker = SlotMaker {
empty_slot: self.imgs.inv_slot,
filled_slot: self.imgs.inv_slot,
selected_slot: self.imgs.inv_slot_sel,
background_color: Some(UI_MAIN),
content_size: ContentSize {
width_height_ratio: 1.0,
max_fraction: 0.75,
},
selected_content_scale: 1.067,
amount_font: self.fonts.cyri.conrod_id,
amount_margins: Vec2::new(-4.0, 0.0),
amount_font_size: self.fonts.cyri.scale(12),
amount_text_color: TEXT_COLOR,
content_source: self.inventory,
image_source: self.item_imgs,
slot_manager: Some(self.slot_manager),
pulse: self.pulse,
};
if state.ids.modular_inputs.len() < 2 {
// Output slot, tags, and modular input slots
let (craft_slot_1, craft_slot_2, can_perform) = match recipe_kind {
RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
if state.ids.craft_slots.len() < 2 {
state.update(|s| {
s.ids
.modular_inputs
.resize(2, &mut ui.widget_id_generator());
s.ids.craft_slots.resize(2, &mut ui.widget_id_generator());
});
}
// Modular Weapon Crafting BG-Art
@ -909,7 +931,7 @@ impl<'a> Widget for Crafting<'a> {
let primary_slot = CraftSlot {
index: 0,
invslot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
requirement: match recipe_kind {
RecipeKind::ModularWeapon => |item, _, _| {
matches!(
@ -932,11 +954,13 @@ impl<'a> Widget for Crafting<'a> {
false
}
},
RecipeKind::Simple => |_, _, _| unreachable!(),
RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
},
info: match recipe_kind {
RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
RecipeKind::ModularWeapon | RecipeKind::Simple => None,
RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
None
},
},
};
@ -945,10 +969,7 @@ impl<'a> Widget for Crafting<'a> {
.top_left_with_margins_on(state.ids.modular_art, 4.0, 4.0)
.parent(state.ids.align_ing);
if let Some(item) = primary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
{
if let Some(item) = primary_slot.item(self.inventory) {
primary_slot_widget
.with_item_tooltip(
self.item_tooltip_manager,
@ -956,7 +977,7 @@ impl<'a> Widget for Crafting<'a> {
&None,
&item_tooltip,
)
.set(state.ids.modular_inputs[0], ui);
.set(state.ids.craft_slots[0], ui);
} else {
let (tooltip_title, tooltip_desc) = match recipe_kind {
RecipeKind::ModularWeapon => (
@ -981,7 +1002,7 @@ impl<'a> Widget for Crafting<'a> {
self.localized_strings
.get_msg("hud-crafting-mod_comp_wood_prim_slot_desc"),
),
RecipeKind::Component(_) | RecipeKind::Simple => {
RecipeKind::Component(_) | RecipeKind::Simple | RecipeKind::Repair => {
(Cow::Borrowed(""), Cow::Borrowed(""))
},
};
@ -993,12 +1014,12 @@ impl<'a> Widget for Crafting<'a> {
&tabs_tooltip,
TEXT_COLOR,
)
.set(state.ids.modular_inputs[0], ui);
.set(state.ids.craft_slots[0], ui);
}
let secondary_slot = CraftSlot {
index: 1,
invslot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
slot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
requirement: match recipe_kind {
RecipeKind::ModularWeapon => |item, _, _| {
matches!(
@ -1021,11 +1042,13 @@ impl<'a> Widget for Crafting<'a> {
false
}
},
RecipeKind::Simple => |_, _, _| unreachable!(),
RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
},
info: match recipe_kind {
RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
RecipeKind::ModularWeapon | RecipeKind::Simple => None,
RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
None
},
},
};
@ -1034,10 +1057,7 @@ impl<'a> Widget for Crafting<'a> {
.top_right_with_margins_on(state.ids.modular_art, 4.0, 4.0)
.parent(state.ids.align_ing);
if let Some(item) = secondary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
{
if let Some(item) = secondary_slot.item(self.inventory) {
secondary_slot_widget
.with_item_tooltip(
self.item_tooltip_manager,
@ -1045,7 +1065,7 @@ impl<'a> Widget for Crafting<'a> {
&None,
&item_tooltip,
)
.set(state.ids.modular_inputs[1], ui);
.set(state.ids.craft_slots[1], ui);
} else {
let (tooltip_title, tooltip_desc) = match recipe_kind {
RecipeKind::ModularWeapon => (
@ -1060,7 +1080,9 @@ impl<'a> Widget for Crafting<'a> {
self.localized_strings
.get_msg("hud-crafting-mod_comp_sec_slot_desc"),
),
RecipeKind::Simple => (Cow::Borrowed(""), Cow::Borrowed("")),
RecipeKind::Simple | RecipeKind::Repair => {
(Cow::Borrowed(""), Cow::Borrowed(""))
},
};
secondary_slot_widget
.with_tooltip(
@ -1070,11 +1092,11 @@ impl<'a> Widget for Crafting<'a> {
&tabs_tooltip,
TEXT_COLOR,
)
.set(state.ids.modular_inputs[1], ui);
.set(state.ids.craft_slots[1], ui);
}
let prim_item_placed = primary_slot.invslot.is_some();
let sec_item_placed = secondary_slot.invslot.is_some();
let prim_item_placed = primary_slot.slot.is_some();
let sec_item_placed = secondary_slot.slot.is_some();
let prim_icon = match recipe_kind {
RecipeKind::ModularWeapon => self.imgs.icon_primary_comp,
@ -1102,18 +1124,18 @@ impl<'a> Widget for Crafting<'a> {
let bg_col = Color::Rgba(1.0, 1.0, 1.0, 0.4);
if !prim_item_placed {
Image::new(prim_icon)
.middle_of(state.ids.modular_inputs[0])
.middle_of(state.ids.craft_slots[0])
.color(Some(bg_col))
.w_h(34.0, 34.0)
.graphics_for(state.ids.modular_inputs[0])
.graphics_for(state.ids.craft_slots[0])
.set(state.ids.modular_wep_ing_1_bg, ui);
}
if !sec_item_placed {
Image::new(sec_icon)
.middle_of(state.ids.modular_inputs[1])
.middle_of(state.ids.craft_slots[1])
.color(Some(bg_col))
.w_h(50.0, 50.0)
.graphics_for(state.ids.modular_inputs[1])
.graphics_for(state.ids.craft_slots[1])
.set(state.ids.modular_wep_ing_2_bg, ui);
}
@ -1122,10 +1144,8 @@ impl<'a> Widget for Crafting<'a> {
let output_item = match recipe_kind {
RecipeKind::ModularWeapon => {
if let Some((primary_comp, toolkind, hand_restriction)) = primary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
.and_then(|item| {
if let Some((primary_comp, toolkind, hand_restriction)) =
primary_slot.item(self.inventory).and_then(|item| {
if let ItemKind::ModularComponent(
ModularComponent::ToolPrimaryComponent {
toolkind,
@ -1141,8 +1161,7 @@ impl<'a> Widget for Crafting<'a> {
})
{
secondary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
.item(self.inventory)
.filter(|item| {
matches!(
&*item.kind(),
@ -1167,22 +1186,19 @@ impl<'a> Widget for Crafting<'a> {
}
},
RecipeKind::Component(toolkind) => {
if let Some(material) = primary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
.and_then(|item| {
if let Some(material) =
primary_slot.item(self.inventory).and_then(|item| {
item.item_definition_id().itemdef_id().map(String::from)
})
{
let component_key = ComponentKey {
toolkind,
material,
modifier: secondary_slot
.invslot
.and_then(|slot| self.inventory.get(slot))
.and_then(|item| {
modifier: secondary_slot.item(self.inventory).and_then(
|item| {
item.item_definition_id().itemdef_id().map(String::from)
}),
},
),
};
self.client.component_recipe_book().get(&component_key).map(
|component_recipe| {
@ -1193,7 +1209,7 @@ impl<'a> Widget for Crafting<'a> {
None
}
},
RecipeKind::Simple => None,
RecipeKind::Simple | RecipeKind::Repair => None,
};
if let Some(output_item) = output_item {
@ -1219,8 +1235,8 @@ impl<'a> Widget for Crafting<'a> {
)
.set(state.ids.output_img, ui);
(
primary_slot.invslot,
secondary_slot.invslot,
primary_slot.slot,
secondary_slot.slot,
self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
== recipe.craft_sprite,
)
@ -1237,7 +1253,7 @@ impl<'a> Widget for Crafting<'a> {
.w_h(70.0, 70.0)
.graphics_for(state.ids.output_img)
.set(state.ids.modular_wep_empty_bg, ui);
(primary_slot.invslot, secondary_slot.invslot, false)
(primary_slot.slot, secondary_slot.slot, false)
}
},
RecipeKind::Simple => {
@ -1342,6 +1358,140 @@ impl<'a> Widget for Crafting<'a> {
}),
)
},
RecipeKind::Repair => {
if state.ids.craft_slots.len() < 1 {
state.update(|s| {
s.ids.craft_slots.resize(1, &mut ui.widget_id_generator());
});
}
if state.ids.repair_buttons.len() < 2 {
state.update(|s| {
s.ids
.repair_buttons
.resize(2, &mut ui.widget_id_generator());
});
}
// Slot for item to be repaired
let repair_slot = CraftSlot {
index: 0,
slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
requirement: |item, _, _| item.durability().map_or(false, |d| d > 0),
info: None,
};
let repair_slot_widget = slot_maker
.fabricate(repair_slot, [40.0; 2])
.top_left_with_margins_on(state.ids.align_ing, 20.0, 40.0)
.parent(state.ids.align_ing);
if let Some(item) = repair_slot.item(self.inventory) {
repair_slot_widget
.with_item_tooltip(
self.item_tooltip_manager,
core::iter::once(item as &dyn ItemDesc),
&None,
&item_tooltip,
)
.set(state.ids.craft_slots[0], ui);
} else {
repair_slot_widget
.with_tooltip(
self.tooltip_manager,
&self
.localized_strings
.get_msg("hud-crafting-repair_slot_title"),
&self
.localized_strings
.get_msg("hud-crafting-repair_slot_desc"),
&tabs_tooltip,
TEXT_COLOR,
)
.set(state.ids.craft_slots[0], ui);
}
let can_repair = |item: &Item| {
// Check that item needs to be repaired, and that inventory has sufficient
// materials to repair
item.durability().map_or(false, |d| d > 0)
&& self.client.repair_recipe_book().repair_recipe(item).map_or(
false,
|recipe| {
recipe
.inventory_contains_ingredients(item, self.inventory)
.is_ok()
},
)
};
// Repair equipped button
if Button::image(self.imgs.button)
.w_h(105.0, 25.0)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.label(
&self
.localized_strings
.get_msg("hud-crafting-repair_equipped"),
)
.label_y(conrod_core::position::Relative::Scalar(1.0))
.label_color(TEXT_COLOR)
.label_font_size(self.fonts.cyri.scale(12))
.label_font_id(self.fonts.cyri.conrod_id)
.image_color(TEXT_COLOR)
.top_right_with_margins_on(state.ids.align_ing, 20.0, 20.0)
.set(state.ids.repair_buttons[0], ui)
.was_clicked()
{
self.inventory
.equipped_items_with_slot()
.filter(|(_, item)| can_repair(item))
.for_each(|(slot, _)| {
events.push(Event::RepairItem {
slot: Slot::Equip(slot),
});
})
}
// Repair all button
if Button::image(self.imgs.button)
.w_h(105.0, 25.0)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.label(&self.localized_strings.get_msg("hud-crafting-repair_all"))
.label_y(conrod_core::position::Relative::Scalar(1.0))
.label_color(TEXT_COLOR)
.label_font_size(self.fonts.cyri.scale(12))
.label_font_id(self.fonts.cyri.conrod_id)
.image_color(TEXT_COLOR)
.mid_bottom_with_margin_on(state.ids.repair_buttons[0], -45.0)
.set(state.ids.repair_buttons[1], ui)
.was_clicked()
{
self.inventory
.equipped_items_with_slot()
.filter(|(_, item)| can_repair(item))
.for_each(|(slot, _)| {
events.push(Event::RepairItem {
slot: Slot::Equip(slot),
});
});
self.inventory
.slots_with_id()
.filter(|(_, item)| item.as_ref().map_or(false, |i| can_repair(i)))
.for_each(|(slot, _)| {
events.push(Event::RepairItem {
slot: Slot::Inventory(slot),
});
});
}
let can_perform = repair_slot
.item(self.inventory)
.map_or(false, |item| can_repair(item));
(repair_slot.slot, None, can_perform)
},
};
// Craft button
@ -1357,7 +1507,10 @@ impl<'a> Widget for Crafting<'a> {
} else {
self.imgs.button
})
.label(&self.localized_strings.get_msg("hud-crafting-craft"))
.label(&match recipe_kind {
RecipeKind::Repair => self.localized_strings.get_msg("hud-crafting-repair"),
_ => self.localized_strings.get_msg("hud-crafting-craft"),
})
.label_y(conrod_core::position::Relative::Scalar(1.0))
.label_color(if can_perform {
TEXT_COLOR
@ -1379,8 +1532,10 @@ impl<'a> Widget for Crafting<'a> {
{
match recipe_kind {
RecipeKind::ModularWeapon => {
if let (Some(primary_slot), Some(secondary_slot)) =
(modular_primary_slot, modular_secondary_slot)
if let (
Some(Slot::Inventory(primary_slot)),
Some(Slot::Inventory(secondary_slot)),
) = (craft_slot_1, craft_slot_2)
{
events.push(Event::CraftModularWeapon {
primary_slot,
@ -1389,11 +1544,14 @@ impl<'a> Widget for Crafting<'a> {
}
},
RecipeKind::Component(toolkind) => {
if let Some(primary_slot) = modular_primary_slot {
if let Some(Slot::Inventory(primary_slot)) = craft_slot_1 {
events.push(Event::CraftModularWeaponComponent {
toolkind,
material: primary_slot,
modifier: modular_secondary_slot,
modifier: craft_slot_2.and_then(|slot| match slot {
Slot::Inventory(slot) => Some(slot),
Slot::Equip(_) => None,
}),
});
}
},
@ -1401,6 +1559,11 @@ impl<'a> Widget for Crafting<'a> {
recipe_name,
amount: 1,
}),
RecipeKind::Repair => {
if let Some(slot) = craft_slot_1 {
events.push(Event::RepairItem { slot });
}
},
}
}
@ -1470,6 +1633,9 @@ impl<'a> Widget for Crafting<'a> {
RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
t.top_left_with_margins_on(state.ids.align_ing, 325.0, 5.0)
},
RecipeKind::Repair => {
t.top_left_with_margins_on(state.ids.align_ing, 80.0, 5.0)
},
})
.set(state.ids.req_station_title, ui);
let station_img = match recipe.craft_sprite {
@ -1482,6 +1648,7 @@ impl<'a> Widget for Crafting<'a> {
Some(SpriteKind::SpinningWheel) => "SpinningWheel",
Some(SpriteKind::TanningRack) => "TanningRack",
Some(SpriteKind::DismantlingBench) => "DismantlingBench",
Some(SpriteKind::RepairBench) => "RepairBench",
None => "CraftsmanHammer",
_ => "CraftsmanHammer",
};
@ -1506,6 +1673,7 @@ impl<'a> Widget for Crafting<'a> {
Some(SpriteKind::SpinningWheel) => "hud-crafting-spinning_wheel",
Some(SpriteKind::TanningRack) => "hud-crafting-tanning_rack",
Some(SpriteKind::DismantlingBench) => "hud-crafting-salvaging_station",
Some(SpriteKind::RepairBench) => "hud-crafting-repair_bench",
_ => "",
};
Text::new(&self.localized_strings.get_msg(station_name))
@ -1525,7 +1693,7 @@ impl<'a> Widget for Crafting<'a> {
}
// Ingredients Text
// Hack from Sharp to account for iterators not having the same type
let (mut iter_a, mut iter_b, mut iter_c);
let (mut iter_a, mut iter_b, mut iter_c, mut iter_d);
let ingredients = match recipe_kind {
RecipeKind::Simple => {
iter_a = recipe
@ -1539,15 +1707,21 @@ impl<'a> Widget for Crafting<'a> {
&mut iter_b
},
RecipeKind::Component(toolkind) => {
if let Some(material) = modular_primary_slot
.and_then(|slot| self.inventory.get(slot))
if let Some(material) = craft_slot_1
.and_then(|slot| match slot {
Slot::Inventory(slot) => self.inventory.get(slot),
Slot::Equip(_) => None,
})
.and_then(|item| item.item_definition_id().itemdef_id().map(String::from))
{
let component_key = ComponentKey {
toolkind,
material,
modifier: modular_secondary_slot
.and_then(|slot| self.inventory.get(slot))
modifier: craft_slot_2
.and_then(|slot| match slot {
Slot::Inventory(slot) => self.inventory.get(slot),
Slot::Equip(_) => None,
})
.and_then(|item| {
item.item_definition_id().itemdef_id().map(String::from)
}),
@ -1566,6 +1740,24 @@ impl<'a> Widget for Crafting<'a> {
&mut iter_b
}
},
RecipeKind::Repair => {
if let Some(item) = match craft_slot_1 {
Some(Slot::Inventory(slot)) => self.inventory.get(slot),
Some(Slot::Equip(slot)) => self.inventory.equipped(slot),
None => None,
} {
if let Some(recipe) = self.client.repair_recipe_book().repair_recipe(item) {
iter_d = recipe.inputs(item).collect::<Vec<_>>().into_iter();
&mut iter_d as &mut dyn ExactSizeIterator<Item = _>
} else {
iter_b = core::iter::empty();
&mut iter_b
}
} else {
iter_b = core::iter::empty();
&mut iter_b
}
},
};
let num_ingredients = ingredients.len();

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

@ -87,7 +87,11 @@ use common::{
self,
ability::{AuxiliaryAbility, Stance},
fluid_dynamics,
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
inventory::{
slot::{InvSlotId, Slot},
trade_pricing::TradePricing,
CollectFailedReason,
},
item::{
tool::{AbilityContext, ToolKind},
ItemDesc, MaterialStatManifest, Quality,
@ -721,6 +725,10 @@ pub enum Event {
modifier: Option<InvSlotId>,
craft_sprite: Option<Vec3<i32>>,
},
RepairItem {
item: Slot,
sprite_pos: Vec3<i32>,
},
InviteMember(Uid),
AcceptInvite,
DeclineInvite,
@ -914,6 +922,7 @@ impl Show {
self.bag = open;
self.map = false;
self.crafting_fields.salvage = false;
if !open {
self.crafting = false;
}
@ -992,6 +1001,10 @@ impl Show {
self.crafting_fields.craft_sprite,
Some((_, SpriteKind::DismantlingBench))
) && matches!(tab, CraftingTab::Dismantle);
self.crafting_fields.initialize_repair = matches!(
self.crafting_fields.craft_sprite,
Some((_, SpriteKind::RepairBench))
);
}
fn diary(&mut self, open: bool) {
@ -3253,6 +3266,19 @@ impl Hud {
crafting::Event::ClearRecipeInputs => {
self.show.crafting_fields.recipe_inputs.clear();
},
crafting::Event::RepairItem { slot } => {
if let Some(sprite_pos) = self
.show
.crafting_fields
.craft_sprite
.map(|(pos, _sprite)| pos)
{
events.push(Event::RepairItem {
item: slot,
sprite_pos,
});
}
},
}
}
}
@ -3688,7 +3714,6 @@ impl Hud {
// Maintain slot manager
'slot_events: for event in self.slot_manager.maintain(ui_widgets) {
use comp::slot::Slot;
use slots::{AbilitySlot, InventorySlot, SlotKind::*};
let to_slot = |slot_kind| match slot_kind {
Inventory(InventorySlot {
@ -3788,7 +3813,21 @@ impl Hud {
self.show
.crafting_fields
.recipe_inputs
.insert(c.index, i.slot);
.insert(c.index, Slot::Inventory(i.slot));
}
} else if let (Equip(e), Crafting(c)) = (a, b) {
// Add item to crafting input
if inventories
.get(client.entity())
.and_then(|inv| inv.equipped(e))
.map_or(false, |item| {
(c.requirement)(item, client.component_recipe_book(), c.info)
})
{
self.show
.crafting_fields
.recipe_inputs
.insert(c.index, Slot::Equip(e));
}
} else if let (Crafting(c), Inventory(_)) = (a, b) {
// Remove item from crafting input
@ -5043,6 +5082,7 @@ pub fn get_sprite_desc(sprite: SpriteKind, localized_strings: &Localization) ->
SpriteKind::Anvil => "hud-crafting-anvil",
SpriteKind::Cauldron => "hud-crafting-cauldron",
SpriteKind::CookingPot => "hud-crafting-cooking_pot",
SpriteKind::RepairBench => "hud-crafting-repair_bench",
SpriteKind::CraftingBench => "hud-crafting-crafting_bench",
SpriteKind::Forge => "hud-crafting-forge",
SpriteKind::Loom => "hud-crafting-loom",

View File

@ -47,6 +47,8 @@ widget_ids! {
death_message_2,
death_message_1_bg,
death_message_2_bg,
death_message_3,
death_message_3_bg,
death_bg,
// Level up message
level_up,
@ -436,6 +438,12 @@ impl<'a> Skillbar<'a> {
.font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.death_message_2_bg, ui);
Text::new(&self.localized_strings.get_msg("hud_items_lost_dur"))
.mid_bottom_with_margin_on(state.ids.death_message_2_bg, -50.0)
.font_size(self.fonts.cyri.scale(30))
.font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.death_message_3_bg, ui);
Text::new(&self.localized_strings.get_msg("hud-you_died"))
.bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
.font_size(self.fonts.cyri.scale(50))
@ -448,6 +456,12 @@ impl<'a> Skillbar<'a> {
.font_id(self.fonts.cyri.conrod_id)
.color(CRITICAL_HP_COLOR)
.set(state.ids.death_message_2, ui);
Text::new(&self.localized_strings.get_msg("hud_items_lost_dur"))
.bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
.font_size(self.fonts.cyri.scale(30))
.font_id(self.fonts.cyri.conrod_id)
.color(CRITICAL_HP_COLOR)
.set(state.ids.death_message_3, ui);
}
}

View File

@ -9,7 +9,7 @@ use common::{
comp::{
ability::{Ability, AbilityInput, AuxiliaryAbility},
item::tool::{AbilityContext, ToolKind},
slot::InvSlotId,
slot::{InvSlotId, Slot},
ActiveAbilities, Body, CharacterState, Combo, Energy, Inventory, Item, ItemKey, SkillSet,
Stance,
},
@ -276,27 +276,35 @@ impl<'a> SlotKey<AbilitiesSource<'a>, img_ids::Imgs> for AbilitySlot {
#[derive(Clone, Copy)]
pub struct CraftSlot {
pub index: u32,
pub invslot: Option<InvSlotId>,
pub slot: Option<Slot>,
pub requirement: fn(&Item, &ComponentRecipeBook, Option<CraftSlotInfo>) -> bool,
pub info: Option<CraftSlotInfo>,
}
impl CraftSlot {
pub fn item<'a>(&'a self, inv: &'a Inventory) -> Option<&'a Item> {
match self.slot {
Some(Slot::Inventory(slot)) => inv.get(slot),
Some(Slot::Equip(slot)) => inv.equipped(slot),
None => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum CraftSlotInfo {
Tool(ToolKind),
}
impl PartialEq for CraftSlot {
fn eq(&self, other: &Self) -> bool {
(self.index, self.invslot) == (other.index, other.invslot)
}
fn eq(&self, other: &Self) -> bool { (self.index, self.slot) == (other.index, other.slot) }
}
impl Debug for CraftSlot {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("CraftSlot")
.field("index", &self.index)
.field("invslot", &self.invslot)
.field("slot", &self.slot)
.field("requirement", &"fn ptr")
.finish()
}
@ -306,14 +314,11 @@ impl SlotKey<Inventory, ItemImgs> for CraftSlot {
type ImageKey = ItemKey;
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
self.invslot
.and_then(|invslot| source.get(invslot))
.map(|i| (i.into(), None))
self.item(source).map(|i| (i.into(), None))
}
fn amount(&self, source: &Inventory) -> Option<u32> {
self.invslot
.and_then(|invslot| source.get(invslot))
self.item(source)
.map(|item| item.amount())
.filter(|amount| *amount > 1)
}

View File

@ -5,7 +5,8 @@ use common::{
item::{
armor::{Armor, ArmorKind, Protection},
tool::{Hands, Tool, ToolKind},
Effects, ItemDefinitionId, ItemDesc, ItemKind, MaterialKind, MaterialStatManifest,
Effects, Item, ItemDefinitionId, ItemDesc, ItemKind, MaterialKind,
MaterialStatManifest,
},
BuffKind,
},
@ -100,9 +101,9 @@ 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() {
let mut count = 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
@ -118,7 +119,11 @@ pub fn stats_count(item: &dyn ItemDesc, msm: &MaterialStatManifest) -> usize {
},
ItemKind::ModularComponent { .. } => 7,
_ => 0,
};
if item.has_durability() {
count += 1;
}
count
}
pub fn line_count(item: &dyn ItemDesc, msm: &MaterialStatManifest, i18n: &Localization) -> usize {
@ -351,6 +356,14 @@ pub fn protec2string(stat: Protection) -> String {
}
}
/// Gets the durability of an item in a format more intuitive for UI
pub fn item_durability(item: &dyn ItemDesc) -> Option<u32> {
let durability = item
.durability()
.or_else(|| item.has_durability().then_some(0));
durability.map(|d| Item::MAX_DURABILITY - d)
}
pub fn ability_image(imgs: &img_ids::Imgs, ability_id: &str) -> image::Id {
match ability_id {
// Debug stick

View File

@ -165,6 +165,9 @@ impl BlocksOfInterest {
fires.push(pos);
interactables.push((pos, Interaction::Craft(CraftingTab::Dismantle)))
},
Some(SpriteKind::RepairBench) => {
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
},
_ => {},
},
}

View File

@ -1777,6 +1777,30 @@ impl PlayState for SessionState {
HudEvent::SalvageItem { slot, salvage_pos } => {
self.client.borrow_mut().salvage_item(slot, salvage_pos);
},
HudEvent::RepairItem { item, sprite_pos } => {
let slots = {
let client = self.client.borrow();
let slots = (|| {
if let Some(inventory) = client.inventories().get(client.entity()) {
let item = match item {
Slot::Equip(slot) => inventory.equipped(slot),
Slot::Inventory(slot) => inventory.get(slot),
}?;
let repair_recipe =
client.repair_recipe_book().repair_recipe(item)?;
repair_recipe
.inventory_contains_ingredients(item, inventory)
.ok()
} else {
None
}
})();
slots.unwrap_or_default()
};
self.client
.borrow_mut()
.repair_item(item, slots, sprite_pos);
},
HudEvent::InviteMember(uid) => {
self.client.borrow_mut().send_invite(uid, InviteKind::Group);
},

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!(
@ -669,10 +669,24 @@ impl<'a> Widget for ItemTooltip<'a> {
6,
);
if item.has_durability() {
let durability = Item::MAX_DURABILITY - item.durability().unwrap_or(0);
stat_text(
format!(
"{} : {}/{}",
i18n.get_msg("common-stats-durability"),
durability,
Item::MAX_DURABILITY
),
7,
)
}
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);
@ -697,6 +711,13 @@ impl<'a> Widget for ItemTooltip<'a> {
equipped_tool_stats.buff_strength,
);
let tool_durability =
util::item_durability(item).unwrap_or(Item::MAX_DURABILITY);
let equipped_durability =
util::item_durability(equipped_item).unwrap_or(Item::MAX_DURABILITY);
let durability_diff =
util::comparison(tool_durability, equipped_durability);
let mut diff_text = |text: String, color, id_index| {
widget::Text::new(&text)
.align_middle_y_of(state.ids.stats[id_index])
@ -752,11 +773,19 @@ impl<'a> Widget for ItemTooltip<'a> {
);
diff_text(text, buff_strength_diff.1, 6)
}
if tool_durability != equipped_durability {
let text = format!(
"{} {}",
&durability_diff.0,
tool_durability as i32 - equipped_durability as i32
);
diff_text(text, durability_diff.1, 7)
}
}
}
},
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)
@ -873,11 +902,25 @@ impl<'a> Widget for ItemTooltip<'a> {
),
index,
);
index += 1;
}
if let Some(durability) = util::item_durability(item) {
stat_text(
format!(
"{} : {}/{}",
i18n.get_msg("common-stats-durability"),
durability,
Item::MAX_DURABILITY
),
index,
);
}
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,
@ -902,6 +945,11 @@ impl<'a> Widget for ItemTooltip<'a> {
let stealth_diff =
util::option_comparison(&armor_stats.stealth, &equipped_stats.stealth);
let armor_durability = util::item_durability(item);
let equipped_durability = util::item_durability(equipped_item);
let durability_diff =
util::option_comparison(&armor_durability, &equipped_durability);
let mut diff_text = |text: String, color, id_index| {
widget::Text::new(&text)
.align_middle_y_of(state.ids.stats[id_index])
@ -970,6 +1018,14 @@ impl<'a> Widget for ItemTooltip<'a> {
diff_text(text, stealth_diff.1, index);
}
}
index += armor_stats.stealth.is_some() as usize;
if armor_durability != equipped_durability {
let diff = armor_durability.unwrap_or(Item::MAX_DURABILITY) as i32
- equipped_durability.unwrap_or(Item::MAX_DURABILITY) as i32;
let text = format!("{} {}", &durability_diff.0, diff);
diff_text(text, durability_diff.1, index);
}
}
}
},

View File

@ -536,7 +536,7 @@ impl Archetype for House {
center_offset.x,
center_offset.y,
z + 100,
)) % 13
)) % 14
{
0..=1 => SpriteKind::Crate,
2 => SpriteKind::Bench,
@ -550,6 +550,7 @@ impl Archetype for House {
10 => SpriteKind::SpinningWheel,
11 => SpriteKind::TanningRack,
12 => SpriteKind::DismantlingBench,
13 => SpriteKind::RepairBench,
_ => unreachable!(),
};

View File

@ -632,6 +632,7 @@ impl Structure for CliffTower {
SpriteKind::Forge,
SpriteKind::DismantlingBench,
SpriteKind::Anvil,
SpriteKind::RepairBench,
];
for dir in LOCALITY {
let pos = super_center + dir * (width / 3);

View File

@ -1150,6 +1150,7 @@ impl Structure for DesertCityMultiPlot {
SpriteKind::Loom,
SpriteKind::Anvil,
SpriteKind::DismantlingBench,
SpriteKind::RepairBench,
];
'outer: for d in 0..2 {
for dir in NEIGHBORS {

View File

@ -888,6 +888,7 @@ impl Structure for SavannahPit {
SpriteKind::Loom,
SpriteKind::Anvil,
SpriteKind::DismantlingBench,
SpriteKind::RepairBench,
];
'outer: for d in 0..2 {
for dir in NEIGHBORS {

View File

@ -128,6 +128,7 @@ impl Structure for Workshop {
SpriteKind::Loom,
SpriteKind::Anvil,
SpriteKind::DismantlingBench,
SpriteKind::RepairBench,
];
'outer: for d in 0..3 {
for dir in CARDINALS {