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) - 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. - 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 - Starting site can now be chosen during character creation
- Durability loss of equipped items on death
### Changed ### Changed
- Bats move slower and use a simple proportional controller to maintain altitude - Bats move slower and use a simple proportional controller to maintain altitude

View File

@ -25,7 +25,7 @@
craft_sprite: Some(CraftingBench), craft_sprite: Some(CraftingBench),
), ),
"velorite_frag": ( "velorite_frag": (
output: ("common.items.mineral.ore.veloritefrag", 2), output: ("common.items.mineral.ore.veloritefrag", 3),
inputs: [ inputs: [
(Item("common.items.mineral.ore.velorite"), 1, false), (Item("common.items.mineral.ore.velorite"), 1, false),
(Item("common.items.tool.craftsman_hammer"), 0, 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.08, Item("common.items.mineral.gem.ruby")),
(0.1, Item("common.items.mineral.gem.emerald")), (0.1, Item("common.items.mineral.gem.emerald")),
(0.2, Item("common.items.mineral.gem.sapphire")), (0.2, Item("common.items.mineral.gem.sapphire")),
(0.25, Item("common.items.mineral.ore.velorite")), (0.5, Item("common.items.mineral.ore.velorite")),
(0.5, Item("common.items.mineral.ore.veloritefrag")),
(0.75, Item("common.items.mineral.gem.topaz")), (0.75, Item("common.items.mineral.gem.topaz")),
(1.0, Item("common.items.mineral.gem.amethyst")), (1.0, Item("common.items.mineral.gem.amethyst")),
(2.0, Item("common.items.mineral.ore.veloritefrag")),
(4.0, Item("common.items.crafting_ing.stones")), (4.0, Item("common.items.crafting_ing.stones")),
// Ores // Ores

View File

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

View File

@ -3,6 +3,9 @@ hud-crafting-recipes = Recipes
hud-crafting-ingredients = Ingredients: hud-crafting-ingredients = Ingredients:
hud-crafting-craft = Craft hud-crafting-craft = Craft
hud-crafting-craft_all = Craft All 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-tool_cata = Requires:
hud-crafting-req_crafting_station = Requires: hud-crafting-req_crafting_station = Requires:
hud-crafting-anvil = Anvil hud-crafting-anvil = Anvil
@ -14,6 +17,7 @@ hud-crafting-loom = Loom
hud-crafting-spinning_wheel = Spinning Wheel hud-crafting-spinning_wheel = Spinning Wheel
hud-crafting-tanning_rack = Tanning Rack hud-crafting-tanning_rack = Tanning Rack
hud-crafting-salvaging_station = Salvaging Bench hud-crafting-salvaging_station = Salvaging Bench
hud-crafting-repair_bench = Repair Bench
hud-crafting-campfire = Campfire hud-crafting-campfire = Campfire
hud-crafting-tabs-all = All hud-crafting-tabs-all = All
hud-crafting-tabs-armor = Armor hud-crafting-tabs-armor = Armor
@ -43,3 +47,5 @@ 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_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_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_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_keybindings_fmt = Press { $key } to toggle keybindings
hud-press_key_to_toggle_debug_info_fmt = Press { $key } to toggle debug info 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-press_key_to_respawn = Press { $key } to respawn at the last campfire you visited.
hud-tutorial_btn = Tutorial hud-tutorial_btn = Tutorial
hud-tutorial_click_here = Press [ { $key } ] to free your cursor and click this button! 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", "voxel.sprite.salvaging_station.salvaging_station-0",
(0.0, 0.0, 0.0), (-50.0, 40.0, 30.0), 0.9, (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 // Weapons
// Diary Example Images // Diary Example Images
Simple("example_utility"): VoxTrans( Simple("example_utility"): VoxTrans(
@ -3155,7 +3159,7 @@
(0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8, (0.0, -1.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
), ),
Simple("common.items.mineral.ore.veloritefrag"): VoxTrans( 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, (0.0, 0.0, 0.0), (-50.0, 40.0, 20.0), 0.8,
), ),
Simple("common.items.food.apple_mushroom_curry"): VoxTrans( Simple("common.items.food.apple_mushroom_curry"): VoxTrans(

View File

@ -447,7 +447,7 @@
color: None color: None
), ),
(Danari, Male, "common.items.armor.misc.head.straw"): ( (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 color: None
), ),
(Danari, Female, "common.items.armor.misc.head.straw"): ( (Danari, Female, "common.items.armor.misc.head.straw"): (
@ -463,7 +463,7 @@
color: None color: None
), ),
(Orc, Male, "common.items.armor.misc.head.straw"): ( (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 color: None
), ),
(Orc, Female, "common.items.armor.misc.head.straw"): ( (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.blue_cheese"): "voxel.object.blue_cheese",
Simple("common.items.food.mushroom"): "voxel.sprite.mushrooms.mushroom-10", 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.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.apple_mushroom_curry"): "voxel.object.mushroom_curry",
Simple("common.items.food.spore_corruption"): "voxel.sprite.spore.corruption_spore", Simple("common.items.food.spore_corruption"): "voxel.sprite.spore.corruption_spore",
Simple("common.items.food.apple_stick"): "voxel.object.apple_stick", 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: [ variations: [
( (
model: "voxygen.voxel.sprite.velorite.velorite_ore", 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), lod_axes: (1.0, 1.0, 1.0),
), ),
], ],
@ -725,53 +725,8 @@ Velorite: Some((
VeloriteFrag: Some(( VeloriteFrag: Some((
variations: [ variations: [
( (
model: "voxygen.voxel.sprite.velorite.velorite_1", model: "voxygen.voxel.sprite.velorite.velorite",
offset: (-3.0, -5.0, 0.0), offset: (-4.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),
lod_axes: (1.0, 1.0, 1.0), lod_axes: (1.0, 1.0, 1.0),
), ),
], ],
@ -3788,6 +3743,17 @@ CookingPot: Some((
], ],
wind_sway: 0.0, 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 // Ensnaring Vines
EnsnaringVines: Some(( EnsnaringVines: Some((
variations: [ variations: [

View File

@ -38,7 +38,7 @@ use common::{
lod, lod,
mounting::Rider, mounting::Rider,
outcome::Outcome, outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook}, recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{GameMode, PlayerEntity, Time, TimeOfDay}, resources::{GameMode, PlayerEntity, Time, TimeOfDay},
shared_server_config::ServerConstants, shared_server_config::ServerConstants,
spiral::Spiral2d, spiral::Spiral2d,
@ -222,6 +222,7 @@ pub struct Client {
pub chat_mode: ChatMode, pub chat_mode: ChatMode,
recipe_book: RecipeBook, recipe_book: RecipeBook,
component_recipe_book: ComponentRecipeBook, component_recipe_book: ComponentRecipeBook,
repair_recipe_book: RepairRecipeBook,
available_recipes: HashMap<String, Option<SpriteKind>>, available_recipes: HashMap<String, Option<SpriteKind>>,
lod_zones: HashMap<Vec2<i32>, lod::Zone>, lod_zones: HashMap<Vec2<i32>, lod::Zone>,
lod_last_requested: Option<Instant>, lod_last_requested: Option<Instant>,
@ -362,6 +363,7 @@ impl Client {
material_stats, material_stats,
ability_map, ability_map,
server_constants, server_constants,
repair_recipe_book,
} = loop { } = loop {
tokio::select! { tokio::select! {
// Spawn in a blocking thread (leaving the network thread free). This is mostly // Spawn in a blocking thread (leaving the network thread free). This is mostly
@ -655,6 +657,7 @@ impl Client {
world_map.pois, world_map.pois,
recipe_book, recipe_book,
component_recipe_book, component_recipe_book,
repair_recipe_book,
max_group_size, max_group_size,
client_timeout, client_timeout,
)) ))
@ -670,6 +673,7 @@ impl Client {
pois, pois,
recipe_book, recipe_book,
component_recipe_book, component_recipe_book,
repair_recipe_book,
max_group_size, max_group_size,
client_timeout, client_timeout,
) = loop { ) = loop {
@ -708,6 +712,7 @@ impl Client {
pois, pois,
recipe_book, recipe_book,
component_recipe_book, component_recipe_book,
repair_recipe_book,
available_recipes: HashMap::default(), available_recipes: HashMap::default(),
chat_mode: ChatMode::default(), chat_mode: ChatMode::default(),
@ -1146,6 +1151,8 @@ impl Client {
pub fn component_recipe_book(&self) -> &ComponentRecipeBook { &self.component_recipe_book } 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>> { pub fn available_recipes(&self) -> &HashMap<String, Option<SpriteKind>> {
&self.available_recipes &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) { fn update_available_recipes(&mut self) {
self.available_recipes = self self.available_recipes = self
.recipe_book .recipe_book

View File

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

View File

@ -15,7 +15,7 @@ use veloren_common::{
armor::{ArmorKind, Protection}, armor::{ArmorKind, Protection},
modular::{generate_weapon_primary_components, generate_weapons}, modular::{generate_weapon_primary_components, generate_weapons},
tool::{Hands, Tool, ToolKind}, tool::{Hands, Tool, ToolKind},
Item, MaterialStatManifest, DurabilityMultiplier, Item, MaterialStatManifest,
}, },
}, },
generation::{EntityConfig, EntityInfo}, generation::{EntityConfig, EntityInfo},
@ -58,21 +58,23 @@ fn armor_stats() -> Result<(), Box<dyn Error>> {
} }
let msm = &MaterialStatManifest::load().read(); 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::Invincible) => "Invincible".to_string(),
Some(Protection::Normal(value)) => value.to_string(), Some(Protection::Normal(value)) => value.to_string(),
None => "0.0".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::Invincible) => "Invincible".to_string(),
Some(Protection::Normal(value)) => value.to_string(), Some(Protection::Normal(value)) => value.to_string(),
None => "0.0".to_string(), None => "0.0".to_string(),
}; };
let max_energy = armor.stats(msm).energy_max.unwrap_or(0.0).to_string(); let max_energy = armor_stats.energy_max.unwrap_or(0.0).to_string();
let energy_reward = armor.stats(msm).energy_reward.unwrap_or(0.0).to_string(); let energy_reward = armor_stats.energy_reward.unwrap_or(0.0).to_string();
let crit_power = armor.stats(msm).crit_power.unwrap_or(0.0).to_string(); let crit_power = armor_stats.crit_power.unwrap_or(0.0).to_string();
let stealth = armor.stats(msm).stealth.unwrap_or(0.0).to_string(); let stealth = armor_stats.stealth.unwrap_or(0.0).to_string();
wtr.write_record([ wtr.write_record([
item.item_definition_id() item.item_definition_id()
@ -124,14 +126,17 @@ fn weapon_stats() -> Result<(), Box<dyn Error>> {
for item in items.iter() { for item in items.iter() {
if let comp::item::ItemKind::Tool(tool) = &*item.kind() { if let comp::item::ItemKind::Tool(tool) = &*item.kind() {
let power = tool.base_power().to_string(); let dur_mult = DurabilityMultiplier(1.0);
let effect_power = tool.base_effect_power().to_string(); let tool_stats = tool.stats(dur_mult);
let speed = tool.base_speed().to_string();
let crit_chance = tool.base_crit_chance().to_string(); let power = tool_stats.power.to_string();
let range = tool.base_range().to_string(); let effect_power = tool_stats.effect_power.to_string();
let energy_efficiency = tool.base_energy_efficiency().to_string(); let speed = tool_stats.speed.to_string();
let buff_strength = tool.base_buff_strength().to_string(); let crit_chance = tool_stats.crit_chance.to_string();
let equip_time = tool.equip_time().as_secs_f32().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 kind = get_tool_kind(&tool.kind);
let hands = get_tool_hands(tool); 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; const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
let rating = if let ItemKind::Tool(tool) = &*item.kind() { 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? // 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 // 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() inv.equipped_items()
.filter_map(|item| { .filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() { if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).crit_power armor
.stats(msm, item.stats_durability_multiplier())
.crit_power
} else { } else {
None None
} }
@ -1379,7 +1381,9 @@ pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialSt
inv.equipped_items() inv.equipped_items()
.filter_map(|item| { .filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() { if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).energy_reward armor
.stats(msm, item.stats_durability_multiplier())
.energy_reward
} else { } else {
None None
} }
@ -1397,7 +1401,9 @@ pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatM
inv.equipped_items() inv.equipped_items()
.filter_map(|item| { .filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() { if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).energy_max armor
.stats(msm, item.stats_durability_multiplier())
.energy_max
} else { } else {
None None
} }
@ -1433,7 +1439,7 @@ pub fn stealth_multiplier_from_items(
inv.equipped_items() inv.equipped_items()
.filter_map(|item| { .filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() { if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).stealth armor.stats(msm, item.stats_durability_multiplier()).stealth
} else { } else {
None None
} }
@ -1456,7 +1462,9 @@ pub fn compute_protection(
inv.equipped_items() inv.equipped_items()
.filter_map(|item| { .filter_map(|item| {
if let ItemKind::Armor(armor) = &*item.kind() { if let ItemKind::Armor(armor) = &*item.kind() {
armor.stats(msm).protection armor
.stats(msm, item.stats_durability_multiplier())
.protection
} else { } else {
None None
} }

View File

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

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
comp::item::{MaterialStatManifest, Rgb}, comp::item::{DurabilityMultiplier, MaterialStatManifest, Rgb},
terrain::{Block, BlockKind}, terrain::{Block, BlockKind},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -25,6 +25,25 @@ pub enum ArmorKind {
Bag, 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 { impl Armor {
/// Determines whether two pieces of armour are superficially equivalent to /// Determines whether two pieces of armour are superficially equivalent to
/// one another (i.e: one may be substituted for the other in crafting /// one another (i.e: one may be substituted for the other in crafting
@ -212,8 +231,12 @@ pub struct Armor {
impl Armor { impl Armor {
pub fn new(kind: ArmorKind, stats: StatsSource) -> Self { Self { kind, stats } } pub fn new(kind: ArmorKind, stats: StatsSource) -> Self { Self { kind, stats } }
pub fn stats(&self, msm: &MaterialStatManifest) -> Stats { pub fn stats(
match &self.stats { &self,
msm: &MaterialStatManifest,
durability_multiplier: DurabilityMultiplier,
) -> Stats {
let base_stats = match &self.stats {
StatsSource::Direct(stats) => *stats, StatsSource::Direct(stats) => *stats,
StatsSource::FromSet(set) => { StatsSource::FromSet(set) => {
let set_stats = msm.armor_stats(set).unwrap_or_else(Stats::none); let set_stats = msm.armor_stats(set).unwrap_or_else(Stats::none);
@ -237,7 +260,8 @@ impl Armor {
set_stats * multiplier set_stats * multiplier
}, },
} };
base_stats * durability_multiplier.0
} }
#[cfg(test)] #[cfg(test)]

View File

@ -5,11 +5,11 @@ pub mod tool;
// Reexports // Reexports
pub use modular::{MaterialStatManifest, ModularBase, ModularComponent}; 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::{ use crate::{
assets::{self, AssetExt, BoxedError, Error}, assets::{self, AssetExt, BoxedError, Error},
comp::inventory::{item::tool::AbilityMap, InvSlot}, comp::inventory::InvSlot,
effect::Effect, effect::Effect,
recipe::RecipeInput, recipe::RecipeInput,
terrain::Block, terrain::Block,
@ -348,6 +348,21 @@ impl ItemKind {
}; };
result 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>>; pub type ItemId = AtomicCell<Option<NonZeroU64>>;
@ -396,6 +411,10 @@ pub struct Item {
slots: Vec<InvSlot>, slots: Vec<InvSlot>,
item_config: Option<Box<ItemConfig>>, item_config: Option<Box<ItemConfig>>,
hash: u64, 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}; 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() { let abilities = if let Some(set_key) = item.ability_spec() {
if let Some(set) = ability_map.get_ability_set(&set_key) { 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 { } else {
error!( error!(
"Custom ability set: {:?} references non-existent set, falling back to \ "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() tool_default(tool.kind).cloned().unwrap_or_default()
} }
} else if let Some(set) = tool_default(tool.kind) { } 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 { } else {
error!( error!(
"No ability set defined for tool: {:?}, falling back to default ability set.", "No ability set defined for tool: {:?}, falling back to default ability set.",
@ -766,6 +787,8 @@ impl assets::Asset for RawItemDef {
pub struct OperationFailure; pub struct OperationFailure;
impl Item { impl Item {
pub const MAX_DURABILITY: u32 = 8;
// TODO: consider alternatives such as default abilities that can be added to a // TODO: consider alternatives such as default abilities that can be added to a
// loadout when no weapon is present // loadout when no weapon is present
pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") } pub fn empty() -> Self { Item::new_from_asset_expect("common.items.weapons.empty.empty") }
@ -785,7 +808,9 @@ impl Item {
// These fields are updated immediately below // These fields are updated immediately below
item_config: None, item_config: None,
hash: 0, hash: 0,
durability_lost: None,
}; };
item.durability_lost = item.has_durability().then_some(0);
item.update_item_state(ability_map, msm); item.update_item_state(ability_map, msm);
item item
} }
@ -1089,7 +1114,7 @@ impl Item {
ItemBase::Modular(mod_base) => { ItemBase::Modular(mod_base) => {
// TODO: Try to move further upward // TODO: Try to move further upward
let msm = MaterialStatManifest::load().read(); 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)] #[cfg(test)]
pub fn create_test_item_from_kind(kind: ItemKind) -> Self { pub fn create_test_item_from_kind(kind: ItemKind) -> Self {
let ability_map = &AbilityMap::load().read(); let ability_map = &AbilityMap::load().read();
@ -1211,10 +1289,11 @@ pub trait ItemDesc {
fn num_slots(&self) -> u16; fn num_slots(&self) -> u16;
fn item_definition_id(&self) -> ItemDefinitionId<'_>; fn item_definition_id(&self) -> ItemDefinitionId<'_>;
fn tags(&self) -> Vec<ItemTag>; fn tags(&self) -> Vec<ItemTag>;
fn is_modular(&self) -> bool; fn is_modular(&self) -> bool;
fn components(&self) -> &[Item]; 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> { fn tool_info(&self) -> Option<ToolKind> {
if let ItemKind::Tool(tool) = &*self.kind() { if let ItemKind::Tool(tool) = &*self.kind() {
@ -1243,6 +1322,14 @@ impl ItemDesc for Item {
fn is_modular(&self) -> bool { self.is_modular() } fn is_modular(&self) -> bool { self.is_modular() }
fn components(&self) -> &[Item] { self.components() } 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 { impl ItemDesc for ItemDef {
@ -1265,6 +1352,12 @@ impl ItemDesc for ItemDef {
fn is_modular(&self) -> bool { false } fn is_modular(&self) -> bool { false }
fn components(&self) -> &[Item] { &[] } 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 { impl Component for Item {
@ -1278,6 +1371,9 @@ impl Component for ItemDrop {
type Storage = DenseVecStorage<Self>; type Storage = DenseVecStorage<Self>;
} }
#[derive(Copy, Clone, Debug)]
pub struct DurabilityMultiplier(pub f32);
impl<'a, T: ItemDesc + ?Sized> ItemDesc for &'a T { impl<'a, T: ItemDesc + ?Sized> ItemDesc for &'a T {
fn description(&self) -> &str { (*self).description() } 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 is_modular(&self) -> bool { (*self).is_modular() }
fn components(&self) -> &[Item] { (*self).components() } 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 /// Returns all item asset specifiers

View File

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

View File

@ -3,14 +3,13 @@
use crate::{ use crate::{
assets::{self, Asset, AssetExt, AssetHandle}, 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 hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub};
ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub},
time::Duration,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
pub enum ToolKind { pub enum ToolKind {
@ -147,6 +146,20 @@ impl Stats {
let diminished = (self.buff_strength - base + 1.0).log(5.0); let diminished = (self.buff_strength - base + 1.0).log(5.0);
base + diminished 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 { impl Asset for Stats {
@ -171,9 +184,11 @@ impl Add<Stats> for Stats {
} }
} }
} }
impl AddAssign<Stats> for Stats { impl AddAssign<Stats> for Stats {
fn add_assign(&mut self, other: Stats) { *self = *self + other; } fn add_assign(&mut self, other: Stats) { *self = *self + other; }
} }
impl Sub<Stats> for Stats { impl Sub<Stats> for Stats {
type Output = Self; type Output = Self;
@ -190,6 +205,7 @@ impl Sub<Stats> for Stats {
} }
} }
} }
impl Mul<Stats> for Stats { impl Mul<Stats> for Stats {
type Output = Self; type Output = Self;
@ -206,9 +222,11 @@ impl Mul<Stats> for Stats {
} }
} }
} }
impl MulAssign<Stats> for Stats { impl MulAssign<Stats> for Stats {
fn mul_assign(&mut self, other: Stats) { *self = *self * other; } fn mul_assign(&mut self, other: Stats) { *self = *self * other; }
} }
impl Div<f32> for Stats { impl Div<f32> for Stats {
type Output = Self; 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Tool { pub struct Tool {
pub kind: ToolKind, pub kind: ToolKind,
pub hands: Hands, pub hands: Hands,
pub stats: Stats, stats: Stats,
// TODO: item specific abilities // TODO: item specific abilities
} }
@ -259,24 +280,9 @@ impl Tool {
} }
} }
// Keep power between 0.5 and 2.00 pub fn stats(&self, durability_multiplier: DurabilityMultiplier) -> Stats {
pub fn base_power(&self) -> f32 { self.stats.power } self.stats * durability_multiplier
}
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) }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -376,10 +382,16 @@ impl AbilityContext {
impl AbilitySet<AbilityItem> { impl AbilitySet<AbilityItem> {
#[must_use] #[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 { self.map(|a| AbilityItem {
id: a.id, 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()) 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 /// Checks that a slot can hold a given item
pub(super) fn slot_can_hold( pub(super) fn slot_can_hold(
&self, &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)] #[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(&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 /// Replaces the loadout item (if any) in the given EquipSlot with the
/// provided item, returning the item that was previously in the slot. /// provided item, returning the item that was previously in the slot.
pub fn replace_loadout_item( 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 { impl Component for Inventory {

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
use crate::{ use crate::{
assets::{self, AssetExt, AssetHandle}, assets::{self, AssetExt, AssetHandle},
comp::{ comp::{
inventory::slot::InvSlotId, inventory::slot::{InvSlotId, Slot},
item::{ item::{
modular, modular,
tool::{AbilityMap, ToolKind}, tool::{AbilityMap, ToolKind},
ItemBase, ItemDef, ItemDefinitionIdOwned, ItemKind, ItemTag, MaterialStatManifest, ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemKind, ItemTag,
MaterialStatManifest,
}, },
Inventory, Item, Inventory, Item,
}, },
@ -39,6 +40,45 @@ pub enum RecipeInput {
ListSameItem(Vec<Arc<ItemDef>>), 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Recipe { pub struct Recipe {
pub output: (Arc<ItemDef>, u32), pub output: (Arc<ItemDef>, u32),
@ -477,13 +517,11 @@ impl assets::Compound for RecipeBook {
cache: assets::AnyCache, cache: assets::AnyCache,
specifier: &assets::SharedString, specifier: &assets::SharedString,
) -> Result<Self, assets::BoxedError> { ) -> Result<Self, assets::BoxedError> {
#[inline]
fn load_item_def(spec: &(String, u32)) -> Result<(Arc<ItemDef>, u32), assets::Error> { fn load_item_def(spec: &(String, u32)) -> Result<(Arc<ItemDef>, u32), assets::Error> {
let def = Arc::<ItemDef>::load_cloned(&spec.0)?; let def = Arc::<ItemDef>::load_cloned(&spec.0)?;
Ok((def, spec.1)) Ok((def, spec.1))
} }
#[inline]
fn load_recipe_input( fn load_recipe_input(
(input, amount, is_mod_comp): &(RawRecipeInput, u32, bool), (input, amount, is_mod_comp): &(RawRecipeInput, u32, bool),
) -> Result<(RecipeInput, u32, bool), assets::Error> { ) -> Result<(RecipeInput, u32, bool), assets::Error> {
@ -594,65 +632,26 @@ impl ComponentRecipe {
let mut slot_claims = HashMap::new(); let mut slot_claims = HashMap::new();
let mut unsatisfied_requirements = Vec::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 // 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 // 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 // up to quantity needed for the crafting input. If the item either
// cannot be used, or there is insufficient quantity, adds input and // cannot be used, or there is insufficient quantity, adds input and
// number of materials needed to unsatisfied requirements. // number of materials needed to unsatisfied requirements.
handle_requirement( self.material.0.handle_requirement(
self.material.1,
&mut slot_claims, &mut slot_claims,
&mut unsatisfied_requirements, &mut unsatisfied_requirements,
inv, inv,
&self.material.0,
self.material.1,
core::iter::once(material_slot), core::iter::once(material_slot),
); );
if let Some((modifier_input, modifier_amount)) = &self.modifier { if let Some((modifier_input, modifier_amount)) = &self.modifier {
// TODO: Better way to get slot to use that ensures this requirement fails if no // TODO: Better way to get slot to use that ensures this requirement fails if no
// slot provided? // slot provided?
handle_requirement( modifier_input.handle_requirement(
*modifier_amount,
&mut slot_claims, &mut slot_claims,
&mut unsatisfied_requirements, &mut unsatisfied_requirements,
inv, inv,
modifier_input,
*modifier_amount,
core::iter::once(modifier_slot.unwrap_or(InvSlotId::new(0, 0))), 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 }) .filter_map(|(j, slot)| if i as u32 == *j { Some(slot) } else { None })
.copied(); .copied();
// Checks if requirement is met, and if not marks it as unsatisfied // Checks if requirement is met, and if not marks it as unsatisfied
handle_requirement( input.handle_requirement(
*amount,
&mut slot_claims, &mut slot_claims,
&mut unsatisfied_requirements, &mut unsatisfied_requirements,
inv, inv,
input,
*amount,
input_slots, input_slots,
); );
}); });
@ -835,7 +833,6 @@ impl assets::Compound for ComponentRecipeBook {
cache: assets::AnyCache, cache: assets::AnyCache,
specifier: &assets::SharedString, specifier: &assets::SharedString,
) -> Result<Self, assets::BoxedError> { ) -> Result<Self, assets::BoxedError> {
#[inline]
fn create_recipe_key(raw_recipe: &RawComponentRecipe) -> ComponentKey { fn create_recipe_key(raw_recipe: &RawComponentRecipe) -> ComponentKey {
match &raw_recipe.output { match &raw_recipe.output {
RawComponentOutput::ToolPrimaryComponent { toolkind, item: _ } => { RawComponentOutput::ToolPrimaryComponent { toolkind, item: _ } => {
@ -853,7 +850,6 @@ impl assets::Compound for ComponentRecipeBook {
} }
} }
#[inline]
fn load_recipe(raw_recipe: &RawComponentRecipe) -> Result<ComponentRecipe, assets::Error> { fn load_recipe(raw_recipe: &RawComponentRecipe) -> Result<ComponentRecipe, assets::Error> {
let output = match &raw_recipe.output { let output = match &raw_recipe.output {
RawComponentOutput::ToolPrimaryComponent { toolkind: _, item } => { 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> { pub fn default_recipe_book() -> AssetHandle<RecipeBook> {
RecipeBook::load_expect("common.recipe_book") 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") 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 { impl assets::Compound for ReverseComponentRecipeBook {
fn load( fn load(
cache: assets::AnyCache, cache: assets::AnyCache,

View File

@ -335,7 +335,10 @@ pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
footwear = data.inventory.and_then(|inv| { footwear = data.inventory.and_then(|inv| {
inv.equipped(EquipSlot::Armor(ArmorSlot::Feet)) inv.equipped(EquipSlot::Armor(ArmorSlot::Feet))
.map(|armor| match armor.kind().as_ref() { .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, _ => Friction::Normal,
}) })
}); });
@ -719,7 +722,10 @@ pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
data.inventory data.inventory
.and_then(|inv| inv.equipped(equip_slot)) .and_then(|inv| inv.equipped(equip_slot))
.and_then(|item| match &*item.kind() { .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, _ => 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(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
.and_then(|item| { .and_then(|item| {
if let ItemKind::Tool(tool) = &*item.kind() { if let ItemKind::Tool(tool) = &*item.kind() {
Some(tool.base_crit_chance()) Some(tool.stats(item.stats_durability_multiplier()).crit_chance)
} else { } else {
None 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(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
.and_then(|item| { .and_then(|item| {
if let ItemKind::Tool(tool) = &*item.kind() { if let ItemKind::Tool(tool) = &*item.kind() {
Some(tool.stats) Some(tool.stats(item.stats_durability_multiplier()))
} else { } else {
None None
} }

View File

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

View File

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

View File

@ -18,7 +18,7 @@ use common::{
comp::{ comp::{
self, aura, buff, self, aura, buff,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
inventory::item::MaterialStatManifest, inventory::item::{AbilityMap, MaterialStatManifest},
loot_owner::LootOwnerKind, loot_owner::LootOwnerKind,
Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory, Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory,
Player, Poise, Pos, SkillSet, Stats, Player, Poise, Pos, SkillSet, Stats,
@ -511,6 +511,13 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
true 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 should_delete {
if let Some(rtsim_entity) = state if let Some(rtsim_entity) = state
.ecs() .ecs()

View File

@ -12,7 +12,9 @@ use common::{
slot::{self, Slot}, slot::{self, Slot},
}, },
consts::MAX_PICKUP_RANGE, 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}, terrain::{Block, SpriteKind},
trade::Trades, trade::Trades,
uid::Uid, uid::Uid,
@ -856,6 +858,20 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
None 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 // 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, parent_container_item_id,
item_definition_id, item_definition_id,
stack_size, stack_size,
position position,
properties
) AS ( ) AS (
SELECT item_id, SELECT item_id,
parent_container_item_id, parent_container_item_id,
item_definition_id, item_definition_id,
stack_size, stack_size,
position position,
properties
FROM item FROM item
WHERE parent_container_item_id = ?1 WHERE parent_container_item_id = ?1
UNION ALL UNION ALL
@ -84,7 +86,8 @@ pub fn load_items(connection: &Connection, root: i64) -> Result<Vec<Item>, Persi
item.parent_container_item_id, item.parent_container_item_id,
item.item_definition_id, item.item_definition_id,
item.stack_size, item.stack_size,
item.position item.position,
item.properties
FROM item, items_tree FROM item, items_tree
WHERE item.parent_container_item_id = items_tree.item_id 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)?, item_definition_id: row.get(2)?,
stack_size: row.get(3)?, stack_size: row.get(3)?,
position: row.get(4)?, position: row.get(4)?,
properties: row.get(5)?,
}) })
})? })?
.filter_map(Result::ok) .filter_map(Result::ok)
@ -390,6 +394,7 @@ pub fn create_character(
parent_container_item_id: WORLD_PSEUDO_CONTAINER_ID, parent_container_item_id: WORLD_PSEUDO_CONTAINER_ID,
item_definition_id: CHARACTER_PSEUDO_CONTAINER_DEF_ID.to_owned(), item_definition_id: CHARACTER_PSEUDO_CONTAINER_DEF_ID.to_owned(),
position: character_id.to_string(), position: character_id.to_string(),
properties: String::new(),
}, },
Item { Item {
stack_size: 1, stack_size: 1,
@ -397,6 +402,7 @@ pub fn create_character(
parent_container_item_id: character_id, parent_container_item_id: character_id,
item_definition_id: INVENTORY_PSEUDO_CONTAINER_DEF_ID.to_owned(), item_definition_id: INVENTORY_PSEUDO_CONTAINER_DEF_ID.to_owned(),
position: INVENTORY_PSEUDO_CONTAINER_POSITION.to_owned(), position: INVENTORY_PSEUDO_CONTAINER_POSITION.to_owned(),
properties: String::new(),
}, },
Item { Item {
stack_size: 1, stack_size: 1,
@ -404,6 +410,7 @@ pub fn create_character(
parent_container_item_id: character_id, parent_container_item_id: character_id,
item_definition_id: LOADOUT_PSEUDO_CONTAINER_DEF_ID.to_owned(), item_definition_id: LOADOUT_PSEUDO_CONTAINER_DEF_ID.to_owned(),
position: LOADOUT_PSEUDO_CONTAINER_POSITION.to_owned(), position: LOADOUT_PSEUDO_CONTAINER_POSITION.to_owned(),
properties: String::new(),
}, },
]; ];
@ -413,8 +420,9 @@ pub fn create_character(
parent_container_item_id, parent_container_item_id,
item_definition_id, item_definition_id,
stack_size, stack_size,
position) position,
VALUES (?1, ?2, ?3, ?4, ?5)", properties)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?; )?;
for pseudo_container in pseudo_containers { for pseudo_container in pseudo_containers {
@ -424,6 +432,7 @@ pub fn create_character(
&pseudo_container.item_definition_id, &pseudo_container.item_definition_id,
&pseudo_container.stack_size, &pseudo_container.stack_size,
&pseudo_container.position, &pseudo_container.position,
&pseudo_container.properties,
])?; ])?;
} }
drop(stmt); drop(stmt);
@ -521,8 +530,9 @@ pub fn create_character(
parent_container_item_id, parent_container_item_id,
item_definition_id, item_definition_id,
stack_size, stack_size,
position) position,
VALUES (?1, ?2, ?3, ?4, ?5)", properties)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?; )?;
for item in inserts { for item in inserts {
@ -532,6 +542,7 @@ pub fn create_character(
&item.model.item_definition_id, &item.model.item_definition_id,
&item.model.stack_size, &item.model.stack_size,
&item.model.position, &item.model.position,
&item.model.properties,
])?; ])?;
} }
drop(stmt); drop(stmt);
@ -1048,8 +1059,9 @@ pub fn update(
parent_container_item_id, parent_container_item_id,
item_definition_id, item_definition_id,
stack_size, stack_size,
position) position,
VALUES (?1, ?2, ?3, ?4, ?5)", properties)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?; )?;
for item in upserted_items.iter() { for item in upserted_items.iter() {
@ -1059,6 +1071,7 @@ pub fn update(
&item.item_definition_id, &item.item_definition_id,
&item.stack_size, &item.stack_size,
&item.position, &item.position,
&item.properties,
])?; ])?;
} }
} }

View File

@ -5,7 +5,10 @@ use crate::persistence::{
use crate::persistence::{ use crate::persistence::{
error::PersistenceError, error::PersistenceError,
json_models::{self, CharacterPosition, DatabaseAbilitySet, GenericBody, HumanoidBody}, json_models::{
self, CharacterPosition, DatabaseAbilitySet, DatabaseItemProperties, GenericBody,
HumanoidBody,
},
}; };
use common::{ use common::{
character::CharacterId, character::CharacterId,
@ -165,6 +168,8 @@ pub fn convert_items_to_database_items(
bfs_queue.push_back((format!("component_{}", i), Some(component), item_id)); 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 { let upsert = ItemModelPair {
model: Item { model: Item {
item_definition_id: String::from(item.persistence_item_id()), item_definition_id: String::from(item.persistence_item_id()),
@ -176,6 +181,8 @@ pub fn convert_items_to_database_items(
} else { } else {
1 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 // Continue to remember the atomic, in case we detect an error later and want
// to roll back to preserve liveness. // 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); item_indices.insert(db_item.item_id, i);
let mut 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: Since this is freshly loaded, the atomic is *unique.* // NOTE: Since this is freshly loaded, the atomic is *unique.*
let comp = item.get_item_id_for_database(); 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() { for (i, db_item) in database_items.iter().enumerate() {
item_indices.insert(db_item.item_id, i); 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. // NOTE: item id is currently *unique*, so we can store the ID safely.
let comp = item.get_item_id_for_database(); let comp = item.get_item_id_for_database();

View File

@ -2,7 +2,7 @@ use common::comp;
use common_base::dev_panic; use common_base::dev_panic;
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::string::ToString; use std::{num::NonZeroU32, string::ToString};
use vek::{Vec2, Vec3}; use vek::{Vec2, Vec3};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -285,3 +285,36 @@ pub fn active_abilities_from_db_model(
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
comp::ability::ActiveAbilities::new(ability_sets) 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 item_definition_id: String,
pub stack_size: i32, pub stack_size: i32,
pub position: String, pub position: String,
pub properties: String,
} }
pub struct Body { pub struct Body {

View File

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

View File

@ -22,7 +22,7 @@ use common::{
Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, MaterialStatManifest, Quality, Item, ItemBase, ItemDef, ItemDesc, ItemKind, ItemTag, MaterialStatManifest, Quality,
TagExampleInfo, TagExampleInfo,
}, },
slot::InvSlotId, slot::{InvSlotId, Slot},
Inventory, Inventory,
}, },
recipe::{ComponentKey, Recipe, RecipeInput}, recipe::{ComponentKey, Recipe, RecipeInput},
@ -86,8 +86,8 @@ widget_ids! {
dismantle_title, dismantle_title,
dismantle_img, dismantle_img,
dismantle_txt, dismantle_txt,
dismantle_highlight_txt, repair_buttons[],
modular_inputs[], craft_slots[],
modular_art, modular_art,
modular_desc_txt, modular_desc_txt,
modular_wep_empty_bg, modular_wep_empty_bg,
@ -115,6 +115,9 @@ pub enum Event {
Focus(widget::Id), Focus(widget::Id),
SearchRecipe(Option<String>), SearchRecipe(Option<String>),
ClearRecipeInputs, ClearRecipeInputs,
RepairItem {
slot: Slot,
},
} }
pub struct CraftingShow { pub struct CraftingShow {
@ -122,8 +125,9 @@ pub struct CraftingShow {
pub crafting_search_key: Option<String>, pub crafting_search_key: Option<String>,
pub craft_sprite: Option<(Vec3<i32>, SpriteKind)>, pub craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
pub salvage: bool, pub salvage: bool,
pub initialize_repair: bool,
// TODO: Maybe try to do something that doesn't need to allocate? // 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 { impl Default for CraftingShow {
@ -133,6 +137,7 @@ impl Default for CraftingShow {
crafting_search_key: None, crafting_search_key: None,
craft_sprite: None, craft_sprite: None,
salvage: false, salvage: false,
initialize_repair: false,
recipe_inputs: HashMap::new(), recipe_inputs: HashMap::new(),
} }
} }
@ -207,7 +212,7 @@ pub enum CraftingTab {
Bag, Bag,
Utility, Utility,
Glider, Glider,
Dismantle, // Needs to be the last one or widget alignment will be messed up Dismantle,
} }
impl CraftingTab { impl CraftingTab {
@ -239,7 +244,8 @@ impl CraftingTab {
CraftingTab::Weapon => imgs.icon_weapon, CraftingTab::Weapon => imgs.icon_weapon,
CraftingTab::Bag => imgs.icon_bag, CraftingTab::Bag => imgs.icon_bag,
CraftingTab::ProcessedMaterial => imgs.icon_processed_material, 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 { pub struct State {
@ -313,6 +323,16 @@ impl<'a> Widget for Crafting<'a> {
let mut events = Vec::new(); 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 // Tooltips
let item_tooltip = ItemTooltip::new( 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; let sel_crafting_tab = &self.show.crafting_fields.crafting_tab;
for (i, crafting_tab) in CraftingTab::iter().enumerate() { for (i, crafting_tab) in CraftingTab::iter()
if crafting_tab != CraftingTab::Dismantle { .filter(|tab| !tab.is_adhoc())
let tab_img = crafting_tab.img_id(self.imgs); .enumerate()
// Button Background {
let mut bg = Image::new(self.imgs.pixel) let tab_img = crafting_tab.img_id(self.imgs);
.w_h(40.0, 30.0) // Button Background
.color(Some(UI_MAIN)); let mut bg = Image::new(self.imgs.pixel)
if i == 0 { .w_h(40.0, 30.0)
bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0) .color(Some(UI_MAIN));
} else { if i == 0 {
bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0) bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0)
}; } else {
bg.set(state.ids.category_bgs[i], ui); bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0)
// Category Button };
if Button::image(if crafting_tab == *sel_crafting_tab { bg.set(state.ids.category_bgs[i], ui);
self.imgs.wpn_icon_border_pressed // Category Button
} else { if Button::image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border self.imgs.wpn_icon_border_pressed
}) } else {
.wh_of(state.ids.category_bgs[i]) self.imgs.wpn_icon_border
.middle_of(state.ids.category_bgs[i]) })
.hover_image(if crafting_tab == *sel_crafting_tab { .wh_of(state.ids.category_bgs[i])
self.imgs.wpn_icon_border_pressed .middle_of(state.ids.category_bgs[i])
} else { .hover_image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_mo self.imgs.wpn_icon_border_pressed
}) } else {
.press_image(if crafting_tab == *sel_crafting_tab { self.imgs.wpn_icon_border_mo
self.imgs.wpn_icon_border_pressed })
} else { .press_image(if crafting_tab == *sel_crafting_tab {
self.imgs.wpn_icon_border_press self.imgs.wpn_icon_border_pressed
}) } else {
.with_tooltip( self.imgs.wpn_icon_border_press
self.tooltip_manager, })
&self.localized_strings.get_msg(crafting_tab.name_key()), .with_tooltip(
"", self.tooltip_manager,
&tabs_tooltip, &self.localized_strings.get_msg(crafting_tab.name_key()),
TEXT_COLOR, "",
) &tabs_tooltip,
.set(state.ids.category_tabs[i], ui) TEXT_COLOR,
.was_clicked() )
{ .set(state.ids.category_tabs[i], ui)
events.push(Event::ChangeCraftingTab(crafting_tab)) .was_clicked()
}; {
// Tab images events.push(Event::ChangeCraftingTab(crafting_tab))
Image::new(tab_img) };
.middle_of(state.ids.category_tabs[i]) // Tab images
.w_h(20.0, 20.0) Image::new(tab_img)
.graphics_for(state.ids.category_tabs[i]) .middle_of(state.ids.category_tabs[i])
.set(state.ids.category_imgs[i], ui); .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 // 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 weapon_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
let metal_comp_recipe = make_pseudo_recipe(SpriteKind::Anvil); let metal_comp_recipe = make_pseudo_recipe(SpriteKind::Anvil);
let wood_comp_recipe = make_pseudo_recipe(SpriteKind::CraftingBench); 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 // 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 // the positions of these every tick, so a BTreeMap is necessary to keep it
// ordered. // ordered.
let mut modular_entries = BTreeMap::new(); let mut pseudo_entries = BTreeMap::new();
modular_entries.insert( pseudo_entries.insert(
String::from("veloren.core.pseudo_recipe.modular_weapon"), 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"), 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"), 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"), 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"), 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"), 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"), 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, // First available recipes, then ones with available materials,
@ -605,36 +631,34 @@ impl<'a> Widget for Crafting<'a> {
(name, recipe, is_craftable, has_materials) (name, recipe, is_craftable, has_materials)
}) })
.chain( .chain(
matches!(sel_crafting_tab, CraftingTab::Weapon | CraftingTab::All) pseudo_entries
.then_some( .iter()
modular_entries // Filter by selected tab
.iter() .filter(|(_, (_, _, tab))| *sel_crafting_tab == CraftingTab::All || sel_crafting_tab == tab)
.filter(|(_, (_, output_name))| { // Filter by search filter
match search_filter { .filter(|(_, (_, output_name, _))| {
SearchFilter::None => { match search_filter {
let output_name = output_name.to_lowercase(); SearchFilter::None => {
search_keys let output_name = output_name.to_lowercase();
.iter() search_keys
.all(|&substring| output_name.contains(substring)) .iter()
}, .all(|&substring| output_name.contains(substring))
// TODO: Get input filtering to work here, probably requires },
// checking component recipe book? // TODO: Get input filtering to work here, probably requires
SearchFilter::Input => false, // checking component recipe book?
SearchFilter::Nonexistent => false, SearchFilter::Input => false,
} SearchFilter::Nonexistent => false,
}) }
.map(|(recipe_name, (recipe, _))| { })
( .map(|(recipe_name, (recipe, _, _))| {
recipe_name, (
*recipe, recipe_name,
self.show.crafting_fields.craft_sprite.map(|(_, s)| s) *recipe,
== recipe.craft_sprite, self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
true, == recipe.craft_sprite,
) true,
}), )
) }),
.into_iter()
.flatten(),
) )
.collect(); .collect();
ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials)| { ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials)| {
@ -647,7 +671,7 @@ impl<'a> Widget for Crafting<'a> {
}); });
// Recipe list // 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 { if state.ids.recipe_list_btns.len() < recipe_list_length {
state.update(|state| { state.update(|state| {
state state
@ -702,11 +726,12 @@ impl<'a> Widget for Crafting<'a> {
.press_image(self.imgs.selection_press) .press_image(self.imgs.selection_press)
.image_color(color::rgba(1.0, 0.82, 0.27, 1.0)); .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) { let recipe_name =
*modular_name if let Some((_recipe, pseudo_name, _filter_tab)) = pseudo_entries.get(name) {
} else { *pseudo_name
&recipe.output.0.name } else {
}; &recipe.output.0.name
};
let text = Text::new(recipe_name) let text = Text::new(recipe_name)
.color(if is_craftable { .color(if is_craftable {
@ -735,12 +760,9 @@ impl<'a> Widget for Crafting<'a> {
if state.selected_recipe.as_ref() == Some(name) { if state.selected_recipe.as_ref() == Some(name) {
state.update(|s| s.selected_recipe = None); state.update(|s| s.selected_recipe = None);
} else { } else {
if matches!( if self.show.crafting_fields.crafting_tab.is_adhoc() {
self.show.crafting_fields.crafting_tab, // If current tab is an adhoc tab, and recipe is selected, change to general
CraftingTab::Dismantle // tab
) {
// If current tab is dismantle, and recipe is selected, change to general
// tab, as in dismantle tab recipe gets deselected
events.push(Event::ChangeCraftingTab(CraftingTab::All)); events.push(Event::ChangeCraftingTab(CraftingTab::All));
} }
state.update(|s| s.selected_recipe = Some(name.clone())); 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 // Deselect recipe if current tab is an adhoc tab, elsewhere if recipe selected
// while dismantling, tab is changed to general // while in an adhoc tab, tab is changed to general
if matches!( if self.show.crafting_fields.crafting_tab.is_adhoc() {
self.show.crafting_fields.crafting_tab,
CraftingTab::Dismantle
) {
state.update(|s| s.selected_recipe = None); state.update(|s| s.selected_recipe = None);
} }
// Selected Recipe // Selected Recipe
if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() { if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() {
Some(selected_recipe) => { 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)) Some((selected_recipe, *modular_recipe))
} else { } else {
@ -827,8 +847,10 @@ impl<'a> Widget for Crafting<'a> {
None => None, None => None,
} { } {
let recipe_name = String::from(recipe_name); let recipe_name = String::from(recipe_name);
let title = if let Some((_recipe, modular_name)) = modular_entries.get(&recipe_name) { let title = if let Some((_recipe, pseudo_name, _filter_tab)) =
*modular_name pseudo_entries.get(&recipe_name)
{
*pseudo_name
} else { } else {
&recipe.output.0.name &recipe.output.0.name
}; };
@ -846,6 +868,7 @@ impl<'a> Widget for Crafting<'a> {
ModularWeapon, ModularWeapon,
Component(ToolKind), Component(ToolKind),
Simple, Simple,
Repair,
} }
let recipe_kind = match recipe_name.as_str() { 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" => { "veloren.core.pseudo_recipe.modular_weapon_component.sceptre" => {
RecipeKind::Component(ToolKind::Sceptre) RecipeKind::Component(ToolKind::Sceptre)
}, },
"veloren.core.pseudo_recipe.repair" => RecipeKind::Repair,
_ => RecipeKind::Simple, _ => RecipeKind::Simple,
}; };
// Output slot, tags, and modular input slots let mut slot_maker = SlotMaker {
let (modular_primary_slot, modular_secondary_slot, can_perform) = match recipe_kind { empty_slot: self.imgs.inv_slot,
RecipeKind::ModularWeapon | RecipeKind::Component(_) => { filled_slot: self.imgs.inv_slot,
let mut slot_maker = SlotMaker { selected_slot: self.imgs.inv_slot_sel,
empty_slot: self.imgs.inv_slot, background_color: Some(UI_MAIN),
filled_slot: self.imgs.inv_slot, content_size: ContentSize {
selected_slot: self.imgs.inv_slot_sel, width_height_ratio: 1.0,
background_color: Some(UI_MAIN), max_fraction: 0.75,
content_size: ContentSize { },
width_height_ratio: 1.0, selected_content_scale: 1.067,
max_fraction: 0.75, amount_font: self.fonts.cyri.conrod_id,
}, amount_margins: Vec2::new(-4.0, 0.0),
selected_content_scale: 1.067, amount_font_size: self.fonts.cyri.scale(12),
amount_font: self.fonts.cyri.conrod_id, amount_text_color: TEXT_COLOR,
amount_margins: Vec2::new(-4.0, 0.0), content_source: self.inventory,
amount_font_size: self.fonts.cyri.scale(12), image_source: self.item_imgs,
amount_text_color: TEXT_COLOR, slot_manager: Some(self.slot_manager),
content_source: self.inventory, pulse: self.pulse,
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| { state.update(|s| {
s.ids s.ids.craft_slots.resize(2, &mut ui.widget_id_generator());
.modular_inputs
.resize(2, &mut ui.widget_id_generator());
}); });
} }
// Modular Weapon Crafting BG-Art // Modular Weapon Crafting BG-Art
@ -909,7 +931,7 @@ impl<'a> Widget for Crafting<'a> {
let primary_slot = CraftSlot { let primary_slot = CraftSlot {
index: 0, 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 { requirement: match recipe_kind {
RecipeKind::ModularWeapon => |item, _, _| { RecipeKind::ModularWeapon => |item, _, _| {
matches!( matches!(
@ -932,11 +954,13 @@ impl<'a> Widget for Crafting<'a> {
false false
} }
}, },
RecipeKind::Simple => |_, _, _| unreachable!(), RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
}, },
info: match recipe_kind { info: match recipe_kind {
RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)), 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) .top_left_with_margins_on(state.ids.modular_art, 4.0, 4.0)
.parent(state.ids.align_ing); .parent(state.ids.align_ing);
if let Some(item) = primary_slot if let Some(item) = primary_slot.item(self.inventory) {
.invslot
.and_then(|slot| self.inventory.get(slot))
{
primary_slot_widget primary_slot_widget
.with_item_tooltip( .with_item_tooltip(
self.item_tooltip_manager, self.item_tooltip_manager,
@ -956,7 +977,7 @@ impl<'a> Widget for Crafting<'a> {
&None, &None,
&item_tooltip, &item_tooltip,
) )
.set(state.ids.modular_inputs[0], ui); .set(state.ids.craft_slots[0], ui);
} else { } else {
let (tooltip_title, tooltip_desc) = match recipe_kind { let (tooltip_title, tooltip_desc) = match recipe_kind {
RecipeKind::ModularWeapon => ( RecipeKind::ModularWeapon => (
@ -981,7 +1002,7 @@ impl<'a> Widget for Crafting<'a> {
self.localized_strings self.localized_strings
.get_msg("hud-crafting-mod_comp_wood_prim_slot_desc"), .get_msg("hud-crafting-mod_comp_wood_prim_slot_desc"),
), ),
RecipeKind::Component(_) | RecipeKind::Simple => { RecipeKind::Component(_) | RecipeKind::Simple | RecipeKind::Repair => {
(Cow::Borrowed(""), Cow::Borrowed("")) (Cow::Borrowed(""), Cow::Borrowed(""))
}, },
}; };
@ -993,12 +1014,12 @@ impl<'a> Widget for Crafting<'a> {
&tabs_tooltip, &tabs_tooltip,
TEXT_COLOR, TEXT_COLOR,
) )
.set(state.ids.modular_inputs[0], ui); .set(state.ids.craft_slots[0], ui);
} }
let secondary_slot = CraftSlot { let secondary_slot = CraftSlot {
index: 1, 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 { requirement: match recipe_kind {
RecipeKind::ModularWeapon => |item, _, _| { RecipeKind::ModularWeapon => |item, _, _| {
matches!( matches!(
@ -1021,11 +1042,13 @@ impl<'a> Widget for Crafting<'a> {
false false
} }
}, },
RecipeKind::Simple => |_, _, _| unreachable!(), RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
}, },
info: match recipe_kind { info: match recipe_kind {
RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)), 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) .top_right_with_margins_on(state.ids.modular_art, 4.0, 4.0)
.parent(state.ids.align_ing); .parent(state.ids.align_ing);
if let Some(item) = secondary_slot if let Some(item) = secondary_slot.item(self.inventory) {
.invslot
.and_then(|slot| self.inventory.get(slot))
{
secondary_slot_widget secondary_slot_widget
.with_item_tooltip( .with_item_tooltip(
self.item_tooltip_manager, self.item_tooltip_manager,
@ -1045,7 +1065,7 @@ impl<'a> Widget for Crafting<'a> {
&None, &None,
&item_tooltip, &item_tooltip,
) )
.set(state.ids.modular_inputs[1], ui); .set(state.ids.craft_slots[1], ui);
} else { } else {
let (tooltip_title, tooltip_desc) = match recipe_kind { let (tooltip_title, tooltip_desc) = match recipe_kind {
RecipeKind::ModularWeapon => ( RecipeKind::ModularWeapon => (
@ -1060,7 +1080,9 @@ impl<'a> Widget for Crafting<'a> {
self.localized_strings self.localized_strings
.get_msg("hud-crafting-mod_comp_sec_slot_desc"), .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 secondary_slot_widget
.with_tooltip( .with_tooltip(
@ -1070,11 +1092,11 @@ impl<'a> Widget for Crafting<'a> {
&tabs_tooltip, &tabs_tooltip,
TEXT_COLOR, 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 prim_item_placed = primary_slot.slot.is_some();
let sec_item_placed = secondary_slot.invslot.is_some(); let sec_item_placed = secondary_slot.slot.is_some();
let prim_icon = match recipe_kind { let prim_icon = match recipe_kind {
RecipeKind::ModularWeapon => self.imgs.icon_primary_comp, 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); let bg_col = Color::Rgba(1.0, 1.0, 1.0, 0.4);
if !prim_item_placed { if !prim_item_placed {
Image::new(prim_icon) Image::new(prim_icon)
.middle_of(state.ids.modular_inputs[0]) .middle_of(state.ids.craft_slots[0])
.color(Some(bg_col)) .color(Some(bg_col))
.w_h(34.0, 34.0) .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); .set(state.ids.modular_wep_ing_1_bg, ui);
} }
if !sec_item_placed { if !sec_item_placed {
Image::new(sec_icon) Image::new(sec_icon)
.middle_of(state.ids.modular_inputs[1]) .middle_of(state.ids.craft_slots[1])
.color(Some(bg_col)) .color(Some(bg_col))
.w_h(50.0, 50.0) .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); .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 { let output_item = match recipe_kind {
RecipeKind::ModularWeapon => { RecipeKind::ModularWeapon => {
if let Some((primary_comp, toolkind, hand_restriction)) = primary_slot if let Some((primary_comp, toolkind, hand_restriction)) =
.invslot primary_slot.item(self.inventory).and_then(|item| {
.and_then(|slot| self.inventory.get(slot))
.and_then(|item| {
if let ItemKind::ModularComponent( if let ItemKind::ModularComponent(
ModularComponent::ToolPrimaryComponent { ModularComponent::ToolPrimaryComponent {
toolkind, toolkind,
@ -1141,8 +1161,7 @@ impl<'a> Widget for Crafting<'a> {
}) })
{ {
secondary_slot secondary_slot
.invslot .item(self.inventory)
.and_then(|slot| self.inventory.get(slot))
.filter(|item| { .filter(|item| {
matches!( matches!(
&*item.kind(), &*item.kind(),
@ -1167,22 +1186,19 @@ impl<'a> Widget for Crafting<'a> {
} }
}, },
RecipeKind::Component(toolkind) => { RecipeKind::Component(toolkind) => {
if let Some(material) = primary_slot if let Some(material) =
.invslot primary_slot.item(self.inventory).and_then(|item| {
.and_then(|slot| self.inventory.get(slot))
.and_then(|item| {
item.item_definition_id().itemdef_id().map(String::from) item.item_definition_id().itemdef_id().map(String::from)
}) })
{ {
let component_key = ComponentKey { let component_key = ComponentKey {
toolkind, toolkind,
material, material,
modifier: secondary_slot modifier: secondary_slot.item(self.inventory).and_then(
.invslot |item| {
.and_then(|slot| self.inventory.get(slot))
.and_then(|item| {
item.item_definition_id().itemdef_id().map(String::from) item.item_definition_id().itemdef_id().map(String::from)
}), },
),
}; };
self.client.component_recipe_book().get(&component_key).map( self.client.component_recipe_book().get(&component_key).map(
|component_recipe| { |component_recipe| {
@ -1193,7 +1209,7 @@ impl<'a> Widget for Crafting<'a> {
None None
} }
}, },
RecipeKind::Simple => None, RecipeKind::Simple | RecipeKind::Repair => None,
}; };
if let Some(output_item) = output_item { if let Some(output_item) = output_item {
@ -1219,8 +1235,8 @@ impl<'a> Widget for Crafting<'a> {
) )
.set(state.ids.output_img, ui); .set(state.ids.output_img, ui);
( (
primary_slot.invslot, primary_slot.slot,
secondary_slot.invslot, secondary_slot.slot,
self.show.crafting_fields.craft_sprite.map(|(_, s)| s) self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
== recipe.craft_sprite, == recipe.craft_sprite,
) )
@ -1237,7 +1253,7 @@ impl<'a> Widget for Crafting<'a> {
.w_h(70.0, 70.0) .w_h(70.0, 70.0)
.graphics_for(state.ids.output_img) .graphics_for(state.ids.output_img)
.set(state.ids.modular_wep_empty_bg, ui); .set(state.ids.modular_wep_empty_bg, ui);
(primary_slot.invslot, secondary_slot.invslot, false) (primary_slot.slot, secondary_slot.slot, false)
} }
}, },
RecipeKind::Simple => { 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 // Craft button
@ -1357,7 +1507,10 @@ impl<'a> Widget for Crafting<'a> {
} else { } else {
self.imgs.button 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_y(conrod_core::position::Relative::Scalar(1.0))
.label_color(if can_perform { .label_color(if can_perform {
TEXT_COLOR TEXT_COLOR
@ -1379,8 +1532,10 @@ impl<'a> Widget for Crafting<'a> {
{ {
match recipe_kind { match recipe_kind {
RecipeKind::ModularWeapon => { RecipeKind::ModularWeapon => {
if let (Some(primary_slot), Some(secondary_slot)) = if let (
(modular_primary_slot, modular_secondary_slot) Some(Slot::Inventory(primary_slot)),
Some(Slot::Inventory(secondary_slot)),
) = (craft_slot_1, craft_slot_2)
{ {
events.push(Event::CraftModularWeapon { events.push(Event::CraftModularWeapon {
primary_slot, primary_slot,
@ -1389,11 +1544,14 @@ impl<'a> Widget for Crafting<'a> {
} }
}, },
RecipeKind::Component(toolkind) => { 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 { events.push(Event::CraftModularWeaponComponent {
toolkind, toolkind,
material: primary_slot, 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, recipe_name,
amount: 1, 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(_) => { RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
t.top_left_with_margins_on(state.ids.align_ing, 325.0, 5.0) 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); .set(state.ids.req_station_title, ui);
let station_img = match recipe.craft_sprite { let station_img = match recipe.craft_sprite {
@ -1482,6 +1648,7 @@ impl<'a> Widget for Crafting<'a> {
Some(SpriteKind::SpinningWheel) => "SpinningWheel", Some(SpriteKind::SpinningWheel) => "SpinningWheel",
Some(SpriteKind::TanningRack) => "TanningRack", Some(SpriteKind::TanningRack) => "TanningRack",
Some(SpriteKind::DismantlingBench) => "DismantlingBench", Some(SpriteKind::DismantlingBench) => "DismantlingBench",
Some(SpriteKind::RepairBench) => "RepairBench",
None => "CraftsmanHammer", None => "CraftsmanHammer",
_ => "CraftsmanHammer", _ => "CraftsmanHammer",
}; };
@ -1506,6 +1673,7 @@ impl<'a> Widget for Crafting<'a> {
Some(SpriteKind::SpinningWheel) => "hud-crafting-spinning_wheel", Some(SpriteKind::SpinningWheel) => "hud-crafting-spinning_wheel",
Some(SpriteKind::TanningRack) => "hud-crafting-tanning_rack", Some(SpriteKind::TanningRack) => "hud-crafting-tanning_rack",
Some(SpriteKind::DismantlingBench) => "hud-crafting-salvaging_station", Some(SpriteKind::DismantlingBench) => "hud-crafting-salvaging_station",
Some(SpriteKind::RepairBench) => "hud-crafting-repair_bench",
_ => "", _ => "",
}; };
Text::new(&self.localized_strings.get_msg(station_name)) Text::new(&self.localized_strings.get_msg(station_name))
@ -1525,7 +1693,7 @@ impl<'a> Widget for Crafting<'a> {
} }
// Ingredients Text // Ingredients Text
// Hack from Sharp to account for iterators not having the same type // 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 { let ingredients = match recipe_kind {
RecipeKind::Simple => { RecipeKind::Simple => {
iter_a = recipe iter_a = recipe
@ -1539,15 +1707,21 @@ impl<'a> Widget for Crafting<'a> {
&mut iter_b &mut iter_b
}, },
RecipeKind::Component(toolkind) => { RecipeKind::Component(toolkind) => {
if let Some(material) = modular_primary_slot if let Some(material) = craft_slot_1
.and_then(|slot| self.inventory.get(slot)) .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)) .and_then(|item| item.item_definition_id().itemdef_id().map(String::from))
{ {
let component_key = ComponentKey { let component_key = ComponentKey {
toolkind, toolkind,
material, material,
modifier: modular_secondary_slot modifier: craft_slot_2
.and_then(|slot| self.inventory.get(slot)) .and_then(|slot| match slot {
Slot::Inventory(slot) => self.inventory.get(slot),
Slot::Equip(_) => None,
})
.and_then(|item| { .and_then(|item| {
item.item_definition_id().itemdef_id().map(String::from) item.item_definition_id().itemdef_id().map(String::from)
}), }),
@ -1566,6 +1740,24 @@ impl<'a> Widget for Crafting<'a> {
&mut iter_b &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(); let num_ingredients = ingredients.len();

View File

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

View File

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

View File

@ -47,6 +47,8 @@ widget_ids! {
death_message_2, death_message_2,
death_message_1_bg, death_message_1_bg,
death_message_2_bg, death_message_2_bg,
death_message_3,
death_message_3_bg,
death_bg, death_bg,
// Level up message // Level up message
level_up, level_up,
@ -436,6 +438,12 @@ impl<'a> Skillbar<'a> {
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(state.ids.death_message_2_bg, ui); .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")) 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) .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
.font_size(self.fonts.cyri.scale(50)) .font_size(self.fonts.cyri.scale(50))
@ -448,6 +456,12 @@ impl<'a> Skillbar<'a> {
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(CRITICAL_HP_COLOR) .color(CRITICAL_HP_COLOR)
.set(state.ids.death_message_2, ui); .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::{ comp::{
ability::{Ability, AbilityInput, AuxiliaryAbility}, ability::{Ability, AbilityInput, AuxiliaryAbility},
item::tool::{AbilityContext, ToolKind}, item::tool::{AbilityContext, ToolKind},
slot::InvSlotId, slot::{InvSlotId, Slot},
ActiveAbilities, Body, CharacterState, Combo, Energy, Inventory, Item, ItemKey, SkillSet, ActiveAbilities, Body, CharacterState, Combo, Energy, Inventory, Item, ItemKey, SkillSet,
Stance, Stance,
}, },
@ -276,27 +276,35 @@ impl<'a> SlotKey<AbilitiesSource<'a>, img_ids::Imgs> for AbilitySlot {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct CraftSlot { pub struct CraftSlot {
pub index: u32, pub index: u32,
pub invslot: Option<InvSlotId>, pub slot: Option<Slot>,
pub requirement: fn(&Item, &ComponentRecipeBook, Option<CraftSlotInfo>) -> bool, pub requirement: fn(&Item, &ComponentRecipeBook, Option<CraftSlotInfo>) -> bool,
pub info: Option<CraftSlotInfo>, 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)] #[derive(Clone, Copy, Debug)]
pub enum CraftSlotInfo { pub enum CraftSlotInfo {
Tool(ToolKind), Tool(ToolKind),
} }
impl PartialEq for CraftSlot { impl PartialEq for CraftSlot {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool { (self.index, self.slot) == (other.index, other.slot) }
(self.index, self.invslot) == (other.index, other.invslot)
}
} }
impl Debug for CraftSlot { impl Debug for CraftSlot {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("CraftSlot") f.debug_struct("CraftSlot")
.field("index", &self.index) .field("index", &self.index)
.field("invslot", &self.invslot) .field("slot", &self.slot)
.field("requirement", &"fn ptr") .field("requirement", &"fn ptr")
.finish() .finish()
} }
@ -306,14 +314,11 @@ impl SlotKey<Inventory, ItemImgs> for CraftSlot {
type ImageKey = ItemKey; type ImageKey = ItemKey;
fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> { fn image_key(&self, source: &Inventory) -> Option<(Self::ImageKey, Option<Color>)> {
self.invslot self.item(source).map(|i| (i.into(), None))
.and_then(|invslot| source.get(invslot))
.map(|i| (i.into(), None))
} }
fn amount(&self, source: &Inventory) -> Option<u32> { fn amount(&self, source: &Inventory) -> Option<u32> {
self.invslot self.item(source)
.and_then(|invslot| source.get(invslot))
.map(|item| item.amount()) .map(|item| item.amount())
.filter(|amount| *amount > 1) .filter(|amount| *amount > 1)
} }

View File

@ -5,7 +5,8 @@ use common::{
item::{ item::{
armor::{Armor, ArmorKind, Protection}, armor::{Armor, ArmorKind, Protection},
tool::{Hands, Tool, ToolKind}, tool::{Hands, Tool, ToolKind},
Effects, ItemDefinitionId, ItemDesc, ItemKind, MaterialKind, MaterialStatManifest, Effects, Item, ItemDefinitionId, ItemDesc, ItemKind, MaterialKind,
MaterialStatManifest,
}, },
BuffKind, 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 { pub fn stats_count(item: &dyn ItemDesc, msm: &MaterialStatManifest) -> usize {
match &*item.kind() { let mut count = match &*item.kind() {
ItemKind::Armor(armor) => { 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_reward.is_some() as usize
+ armor_stats.energy_max.is_some() as usize + armor_stats.energy_max.is_some() as usize
+ armor_stats.stealth.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, ItemKind::ModularComponent { .. } => 7,
_ => 0, _ => 0,
};
if item.has_durability() {
count += 1;
} }
count
} }
pub fn line_count(item: &dyn ItemDesc, msm: &MaterialStatManifest, i18n: &Localization) -> usize { 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 { pub fn ability_image(imgs: &img_ids::Imgs, ability_id: &str) -> image::Id {
match ability_id { match ability_id {
// Debug stick // Debug stick

View File

@ -165,6 +165,9 @@ impl BlocksOfInterest {
fires.push(pos); fires.push(pos);
interactables.push((pos, Interaction::Craft(CraftingTab::Dismantle))) 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 } => { HudEvent::SalvageItem { slot, salvage_pos } => {
self.client.borrow_mut().salvage_item(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) => { HudEvent::InviteMember(uid) => {
self.client.borrow_mut().send_invite(uid, InviteKind::Group); self.client.borrow_mut().send_invite(uid, InviteKind::Group);
}, },

View File

@ -583,7 +583,7 @@ impl<'a> Widget for ItemTooltip<'a> {
// Stats // Stats
match &*item.kind() { match &*item.kind() {
ItemKind::Tool(tool) => { ItemKind::Tool(tool) => {
let stats = tool.stats; let stats = tool.stats(item.stats_durability_multiplier());
// Power // Power
widget::Text::new(&format!( widget::Text::new(&format!(
@ -669,10 +669,24 @@ impl<'a> Widget for ItemTooltip<'a> {
6, 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 Some(equipped_item) = equipped_item {
if let ItemKind::Tool(equipped_tool) = &*equipped_item.kind() { if let ItemKind::Tool(equipped_tool) = &*equipped_item.kind() {
let tool_stats = tool.stats; let tool_stats = tool.stats(item.stats_durability_multiplier());
let equipped_tool_stats = equipped_tool.stats; let equipped_tool_stats =
equipped_tool.stats(equipped_item.stats_durability_multiplier());
let diff = tool_stats - equipped_tool_stats; let diff = tool_stats - equipped_tool_stats;
let power_diff = let power_diff =
util::comparison(tool_stats.power, equipped_tool_stats.power); util::comparison(tool_stats.power, equipped_tool_stats.power);
@ -697,6 +711,13 @@ impl<'a> Widget for ItemTooltip<'a> {
equipped_tool_stats.buff_strength, 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| { let mut diff_text = |text: String, color, id_index| {
widget::Text::new(&text) widget::Text::new(&text)
.align_middle_y_of(state.ids.stats[id_index]) .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) 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) => { 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| { let mut stat_text = |text: String, i: usize| {
widget::Text::new(&text) widget::Text::new(&text)
@ -873,11 +902,25 @@ impl<'a> Widget for ItemTooltip<'a> {
), ),
index, 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 Some(equipped_item) = equipped_item {
if let ItemKind::Armor(equipped_armor) = &*equipped_item.kind() { 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 diff = armor_stats - equipped_stats;
let protection_diff = util::option_comparison( let protection_diff = util::option_comparison(
&armor_stats.protection, &armor_stats.protection,
@ -902,6 +945,11 @@ impl<'a> Widget for ItemTooltip<'a> {
let stealth_diff = let stealth_diff =
util::option_comparison(&armor_stats.stealth, &equipped_stats.stealth); 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| { let mut diff_text = |text: String, color, id_index| {
widget::Text::new(&text) widget::Text::new(&text)
.align_middle_y_of(state.ids.stats[id_index]) .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); 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.x,
center_offset.y, center_offset.y,
z + 100, z + 100,
)) % 13 )) % 14
{ {
0..=1 => SpriteKind::Crate, 0..=1 => SpriteKind::Crate,
2 => SpriteKind::Bench, 2 => SpriteKind::Bench,
@ -550,6 +550,7 @@ impl Archetype for House {
10 => SpriteKind::SpinningWheel, 10 => SpriteKind::SpinningWheel,
11 => SpriteKind::TanningRack, 11 => SpriteKind::TanningRack,
12 => SpriteKind::DismantlingBench, 12 => SpriteKind::DismantlingBench,
13 => SpriteKind::RepairBench,
_ => unreachable!(), _ => unreachable!(),
}; };

View File

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

View File

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

View File

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

View File

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