diff --git a/assets/voxygen/i18n/en/hud/crafting.ftl b/assets/voxygen/i18n/en/hud/crafting.ftl index a549ae11b3..c65852f348 100644 --- a/assets/voxygen/i18n/en/hud/crafting.ftl +++ b/assets/voxygen/i18n/en/hud/crafting.ftl @@ -26,12 +26,19 @@ hud-crafting-tabs-utility = Utility hud-crafting-tabs-weapon = Weapons hud-crafting-tabs-bag = Bags hud-crafting-tabs-processed_material = Materials +hud-crafting-tabs-repair = Repair hud-crafting-dismantle_title = Dismantling hud-crafting-dismantle_explanation = Hover items in your bag to see what you can salvage. - + Double-Click them to start dismantling. +hud-crafting-repair_title = Repair Items +hud-crafting-repair_explanation = + Repair your damaged and + broken equipment here. + + Double-Click the item to repair it. hud-crafting-modular_desc = Drag Item-Parts here to craft a weapon hud-crafting-mod_weap_prim_slot_title = Primary Weapon Component hud-crafting-mod_weap_prim_slot_desc = Place a primary weapon component here (e.g. a sword blade, axe head, or bow limbs). diff --git a/client/src/lib.rs b/client/src/lib.rs index 3922e419ce..e170b3ee4c 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1289,6 +1289,34 @@ 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, slot: Slot, sprite_pos: Vec3) -> bool { + let is_repairable = { + let inventories = self.inventories(); + let inventory = inventories.get(self.entity()); + inventory.map_or(false, |inv| { + if let Some(item) = match slot { + 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(slot), + craft_sprite: Some(sprite_pos), + }, + ))); + } + is_repairable + } + fn update_available_recipes(&mut self) { self.available_recipes = self .recipe_book diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 29bed25ef4..704f0fa7d5 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -105,6 +105,7 @@ pub enum CraftEvent { modifier: Option, slots: Vec<(u32, InvSlotId)>, }, + Repair(Slot), } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index e6e88b647d..990c696ee7 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -1244,6 +1244,8 @@ impl Item { } } + pub fn reset_durability(&mut self) { self.durability = self.durability.map(|_| 0); } + #[cfg(test)] pub fn create_test_item_from_kind(kind: ItemKind) -> Self { let ability_map = &AbilityMap::load().read(); diff --git a/common/src/comp/inventory/loadout.rs b/common/src/comp/inventory/loadout.rs index f52826668a..25d45716f3 100644 --- a/common/src/comp/inventory/loadout.rs +++ b/common/src/comp/inventory/loadout.rs @@ -435,6 +435,18 @@ impl Loadout { } }) } + + /// Resets durability of item in specified slot + pub(super) fn repair_item_at_slot(&mut self, equip_slot: EquipSlot) { + 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(); + } + } } #[cfg(test)] diff --git a/common/src/comp/inventory/mod.rs b/common/src/comp/inventory/mod.rs index 26241b6323..f10ac9f4c4 100644 --- a/common/src/comp/inventory/mod.rs +++ b/common/src/comp/inventory/mod.rs @@ -887,6 +887,20 @@ impl Inventory { ) { self.loadout.apply_durability(ability_map, msm) } + + /// Resets durability of item in specified slot + pub fn repair_item_at_slot(&mut self, slot: Slot) { + match slot { + Slot::Inventory(invslot) => { + if let Some(Some(item)) = self.slot_mut(invslot).map(Option::as_mut) { + item.reset_durability(); + } + }, + Slot::Equip(equip_slot) => { + self.loadout.repair_item_at_slot(equip_slot); + }, + } + } } impl Component for Inventory { diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 398720c079..6e8669586b 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -291,6 +291,7 @@ impl Block { | SpriteKind::Loom | SpriteKind::SpinningWheel | SpriteKind::DismantlingBench + | SpriteKind::RepairBench | SpriteKind::TanningRack | SpriteKind::Chest | SpriteKind::DungeonChest0 diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index 74a8f6d205..e15cbb1439 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -240,6 +240,7 @@ make_case_elim!( Keyhole = 0xD7, KeyDoor = 0xD8, CommonLockedChest = 0xD9, + RepairBench = 0xDA, } ); @@ -308,6 +309,7 @@ impl SpriteKind { SpriteKind::CookingPot => 1.36, SpriteKind::DismantlingBench => 1.18, SpriteKind::IceSpike => 1.0, + SpriteKind::RepairBench => 1.2, // TODO: Find suitable heights. SpriteKind::BarrelCactus | SpriteKind::RoundCactus @@ -581,6 +583,7 @@ impl SpriteKind { | SpriteKind::TanningRack | SpriteKind::Loom | SpriteKind::DismantlingBench + | SpriteKind::RepairBench | SpriteKind::ChristmasOrnament | SpriteKind::ChristmasWreath | SpriteKind::WindowArabic diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 5df2c61dfc..558fe863fb 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -856,6 +856,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv None } }, + CraftEvent::Repair(slot) => { + let sprite = get_craft_sprite(state, craft_sprite); + if matches!(sprite, Some(SpriteKind::RepairBench)) { + inventory.repair_item_at_slot(slot); + } + None + }, }; // Attempt to insert items into inventory, dropping them if there is not enough diff --git a/voxygen/src/hud/crafting.rs b/voxygen/src/hud/crafting.rs index 8c59e8c715..c344e4514b 100644 --- a/voxygen/src/hud/crafting.rs +++ b/voxygen/src/hud/crafting.rs @@ -86,7 +86,9 @@ widget_ids! { dismantle_title, dismantle_img, dismantle_txt, - dismantle_highlight_txt, + repair_title, + repair_img, + repair_txt, modular_inputs[], modular_art, modular_desc_txt, @@ -122,6 +124,7 @@ pub struct CraftingShow { pub crafting_search_key: Option, pub craft_sprite: Option<(Vec3, SpriteKind)>, pub salvage: bool, + pub repair: bool, // TODO: Maybe try to do something that doesn't need to allocate? pub recipe_inputs: HashMap, } @@ -133,6 +136,7 @@ impl Default for CraftingShow { crafting_search_key: None, craft_sprite: None, salvage: false, + repair: false, recipe_inputs: HashMap::new(), } } @@ -207,7 +211,8 @@ pub enum CraftingTab { Bag, Utility, Glider, - Dismantle, // Needs to be the last one or widget alignment will be messed up + Dismantle, + Repair, } impl CraftingTab { @@ -224,6 +229,7 @@ impl CraftingTab { CraftingTab::Bag => "hud-crafting-tabs-bag", CraftingTab::ProcessedMaterial => "hud-crafting-tabs-processed_material", CraftingTab::Dismantle => "hud-crafting-tabs-dismantle", + CraftingTab::Repair => "hud-crafting-tabs-repair", } } @@ -239,14 +245,16 @@ impl CraftingTab { CraftingTab::Weapon => imgs.icon_weapon, CraftingTab::Bag => imgs.icon_bag, CraftingTab::ProcessedMaterial => imgs.icon_processed_material, - CraftingTab::Dismantle => imgs.icon_dismantle, + // These tabs are never shown, so using not found is fine + CraftingTab::Dismantle => imgs.not_found, + CraftingTab::Repair => imgs.not_found, } } fn satisfies(self, recipe: &Recipe) -> bool { let (item, _count) = &recipe.output; match self { - CraftingTab::All | CraftingTab::Dismantle => true, + CraftingTab::All | CraftingTab::Dismantle | CraftingTab::Repair => true, CraftingTab::Food => item.tags().contains(&ItemTag::Food), CraftingTab::Armor => match &*item.kind() { ItemKind::Armor(_) => !item.tags().contains(&ItemTag::Bag), @@ -271,6 +279,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 | CraftingTab::Repair) } } pub struct State { @@ -434,56 +446,57 @@ impl<'a> Widget for Crafting<'a> { }) }; let sel_crafting_tab = &self.show.crafting_fields.crafting_tab; - for (i, crafting_tab) in CraftingTab::iter().enumerate() { - if crafting_tab != CraftingTab::Dismantle { - let tab_img = crafting_tab.img_id(self.imgs); - // Button Background - let mut bg = Image::new(self.imgs.pixel) - .w_h(40.0, 30.0) - .color(Some(UI_MAIN)); - if i == 0 { - bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0) - } else { - bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0) - }; - bg.set(state.ids.category_bgs[i], ui); - // Category Button - if Button::image(if crafting_tab == *sel_crafting_tab { - self.imgs.wpn_icon_border_pressed - } else { - self.imgs.wpn_icon_border - }) - .wh_of(state.ids.category_bgs[i]) - .middle_of(state.ids.category_bgs[i]) - .hover_image(if crafting_tab == *sel_crafting_tab { - self.imgs.wpn_icon_border_pressed - } else { - self.imgs.wpn_icon_border_mo - }) - .press_image(if crafting_tab == *sel_crafting_tab { - self.imgs.wpn_icon_border_pressed - } else { - self.imgs.wpn_icon_border_press - }) - .with_tooltip( - self.tooltip_manager, - &self.localized_strings.get_msg(crafting_tab.name_key()), - "", - &tabs_tooltip, - TEXT_COLOR, - ) - .set(state.ids.category_tabs[i], ui) - .was_clicked() - { - events.push(Event::ChangeCraftingTab(crafting_tab)) - }; - // Tab images - Image::new(tab_img) - .middle_of(state.ids.category_tabs[i]) - .w_h(20.0, 20.0) - .graphics_for(state.ids.category_tabs[i]) - .set(state.ids.category_imgs[i], ui); - } + for (i, crafting_tab) in CraftingTab::iter() + .filter(|tab| !tab.is_adhoc()) + .enumerate() + { + let tab_img = crafting_tab.img_id(self.imgs); + // Button Background + let mut bg = Image::new(self.imgs.pixel) + .w_h(40.0, 30.0) + .color(Some(UI_MAIN)); + if i == 0 { + bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0) + } else { + bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0) + }; + bg.set(state.ids.category_bgs[i], ui); + // Category Button + if Button::image(if crafting_tab == *sel_crafting_tab { + self.imgs.wpn_icon_border_pressed + } else { + self.imgs.wpn_icon_border + }) + .wh_of(state.ids.category_bgs[i]) + .middle_of(state.ids.category_bgs[i]) + .hover_image(if crafting_tab == *sel_crafting_tab { + self.imgs.wpn_icon_border_pressed + } else { + self.imgs.wpn_icon_border_mo + }) + .press_image(if crafting_tab == *sel_crafting_tab { + self.imgs.wpn_icon_border_pressed + } else { + self.imgs.wpn_icon_border_press + }) + .with_tooltip( + self.tooltip_manager, + &self.localized_strings.get_msg(crafting_tab.name_key()), + "", + &tabs_tooltip, + TEXT_COLOR, + ) + .set(state.ids.category_tabs[i], ui) + .was_clicked() + { + events.push(Event::ChangeCraftingTab(crafting_tab)) + }; + // Tab images + Image::new(tab_img) + .middle_of(state.ids.category_tabs[i]) + .w_h(20.0, 20.0) + .graphics_for(state.ids.category_tabs[i]) + .set(state.ids.category_imgs[i], ui); } // TODO: Consider UX for filtering searches, maybe a checkbox or a dropdown if @@ -735,12 +748,9 @@ impl<'a> Widget for Crafting<'a> { if state.selected_recipe.as_ref() == Some(name) { state.update(|s| s.selected_recipe = None); } else { - if matches!( - self.show.crafting_fields.crafting_tab, - CraftingTab::Dismantle - ) { - // If current tab is dismantle, and recipe is selected, change to general - // tab, as in dismantle tab recipe gets deselected + if self.show.crafting_fields.crafting_tab.is_adhoc() { + // If current tab is an adhoc tab, and recipe is selected, change to general + // tab events.push(Event::ChangeCraftingTab(CraftingTab::All)); } state.update(|s| s.selected_recipe = Some(name.clone())); @@ -802,12 +812,9 @@ impl<'a> Widget for Crafting<'a> { } } - // Deselect recipe if current tab is dismantle, elsewhere if recipe selected - // while dismantling, tab is changed to general - if matches!( - self.show.crafting_fields.crafting_tab, - CraftingTab::Dismantle - ) { + // Deselect recipe if current tab is an adhoc tab, elsewhere if recipe selected + // while in an adhoc tab, tab is changed to general + if self.show.crafting_fields.crafting_tab.is_adhoc() { state.update(|s| s.selected_recipe = None); } @@ -1854,6 +1861,42 @@ impl<'a> Widget for Crafting<'a> { .color(TEXT_COLOR) .parent(state.ids.window) .set(state.ids.dismantle_txt, ui); + } else if *sel_crafting_tab == CraftingTab::Repair { + // Title + Text::new(&self.localized_strings.get_msg("hud-crafting-repair_title")) + .mid_top_with_margin_on(state.ids.align_ing, 0.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(24)) + .color(TEXT_COLOR) + .parent(state.ids.window) + .set(state.ids.repair_title, ui); + + // Bench Icon + let size = 140.0; + Image::new(animate_by_pulse( + &self + .item_imgs + .img_ids_or_not_found_img(ItemKey::Simple("RepairBench".to_string())), + self.pulse, + )) + .wh([size; 2]) + .mid_top_with_margin_on(state.ids.align_ing, 50.0) + .parent(state.ids.align_ing) + .set(state.ids.repair_img, ui); + + // Explanation + + Text::new( + &self + .localized_strings + .get_msg("hud-crafting-repair_explanation"), + ) + .mid_bottom_with_margin_on(state.ids.repair_img, -60.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .color(TEXT_COLOR) + .parent(state.ids.window) + .set(state.ids.repair_txt, ui); } // Search / Title Recipes diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 375a8f0a73..669fe0ca68 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -87,7 +87,11 @@ use common::{ self, ability::{AuxiliaryAbility, Stance}, fluid_dynamics, - inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason}, + inventory::{ + slot::{InvSlotId, Slot}, + trade_pricing::TradePricing, + CollectFailedReason, + }, item::{ tool::{AbilityContext, ToolKind}, ItemDesc, MaterialStatManifest, Quality, @@ -721,6 +725,10 @@ pub enum Event { modifier: Option, craft_sprite: Option>, }, + RepairItem { + slot: Slot, + sprite_pos: Vec3, + }, InviteMember(Uid), AcceptInvite, DeclineInvite, @@ -914,6 +922,8 @@ impl Show { self.bag = open; self.map = false; self.crafting_fields.salvage = false; + self.crafting_fields.repair = false; + if !open { self.crafting = false; } @@ -937,6 +947,7 @@ impl Show { self.bag = false; self.crafting = false; self.crafting_fields.salvage = false; + self.crafting_fields.repair = false; self.social = false; self.quest = false; self.diary = false; @@ -973,6 +984,7 @@ impl Show { } self.crafting = open; self.crafting_fields.salvage = false; + self.crafting_fields.repair = false; self.crafting_fields.recipe_inputs = HashMap::new(); self.bag = open; self.map = false; @@ -992,6 +1004,10 @@ impl Show { self.crafting_fields.craft_sprite, Some((_, SpriteKind::DismantlingBench)) ) && matches!(tab, CraftingTab::Dismantle); + self.crafting_fields.repair = matches!( + self.crafting_fields.craft_sprite, + Some((_, SpriteKind::RepairBench)) + ) && matches!(tab, CraftingTab::Repair); } fn diary(&mut self, open: bool) { @@ -1000,6 +1016,7 @@ impl Show { self.quest = false; self.crafting = false; self.crafting_fields.salvage = false; + self.crafting_fields.repair = false; self.bag = false; self.map = false; self.diary_fields = diary::DiaryShow::default(); @@ -1020,6 +1037,7 @@ impl Show { self.quest = false; self.crafting = false; self.crafting_fields.salvage = false; + self.crafting_fields.repair = false; self.diary = false; self.want_grab = !self.any_window_requires_cursor(); } @@ -3688,7 +3706,6 @@ impl Hud { // Maintain slot manager 'slot_events: for event in self.slot_manager.maintain(ui_widgets) { - use comp::slot::Slot; use slots::{AbilitySlot, InventorySlot, SlotKind::*}; let to_slot = |slot_kind| match slot_kind { Inventory(InventorySlot { @@ -3920,6 +3937,17 @@ impl Hud { { events.push(Event::SalvageItem { slot, salvage_pos }) } + } else if self.show.crafting_fields.repair + && matches!(self.show.crafting_fields.crafting_tab, CraftingTab::Repair) + { + if let Some((sprite_pos, _sprite_kind)) = + self.show.crafting_fields.craft_sprite + { + events.push(Event::RepairItem { + slot: from, + sprite_pos, + }) + } } else { events.push(Event::UseSlot { slot: from, diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index b783cb07cc..d73ee86a95 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -165,6 +165,9 @@ impl BlocksOfInterest { fires.push(pos); interactables.push((pos, Interaction::Craft(CraftingTab::Dismantle))) }, + Some(SpriteKind::RepairBench) => { + interactables.push((pos, Interaction::Craft(CraftingTab::Repair))) + }, _ => {}, }, } diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 4aef117d82..7c366d4fdf 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -1777,6 +1777,9 @@ impl PlayState for SessionState { HudEvent::SalvageItem { slot, salvage_pos } => { self.client.borrow_mut().salvage_item(slot, salvage_pos); }, + HudEvent::RepairItem { slot, sprite_pos } => { + self.client.borrow_mut().repair_item(slot, sprite_pos); + }, HudEvent::InviteMember(uid) => { self.client.borrow_mut().send_invite(uid, InviteKind::Group); }, diff --git a/voxygen/src/ui/widgets/item_tooltip.rs b/voxygen/src/ui/widgets/item_tooltip.rs index d3d50c664e..c45868afa7 100644 --- a/voxygen/src/ui/widgets/item_tooltip.rs +++ b/voxygen/src/ui/widgets/item_tooltip.rs @@ -675,7 +675,7 @@ impl<'a> Widget for ItemTooltip<'a> { let durability = MAX_DURABILITY - durability.min(MAX_DURABILITY); widget::Text::new(&format!( "{} : {}/{}", - i18n.get("common.stats.durability"), + i18n.get_msg("common-stats-durability"), durability, MAX_DURABILITY )) @@ -900,7 +900,7 @@ impl<'a> Widget for ItemTooltip<'a> { stat_text( format!( "{} : {}/{}", - i18n.get("common.stats.durability"), + i18n.get_msg("common-stats-durability"), durability, Item::MAX_DURABILITY ), diff --git a/world/src/site/settlement/building/archetype/house.rs b/world/src/site/settlement/building/archetype/house.rs index 074b442343..6904f5093b 100644 --- a/world/src/site/settlement/building/archetype/house.rs +++ b/world/src/site/settlement/building/archetype/house.rs @@ -536,7 +536,7 @@ impl Archetype for House { center_offset.x, center_offset.y, z + 100, - )) % 13 + )) % 14 { 0..=1 => SpriteKind::Crate, 2 => SpriteKind::Bench, @@ -550,6 +550,7 @@ impl Archetype for House { 10 => SpriteKind::SpinningWheel, 11 => SpriteKind::TanningRack, 12 => SpriteKind::DismantlingBench, + 13 => SpriteKind::RepairBench, _ => unreachable!(), }; diff --git a/world/src/site2/plot/workshop.rs b/world/src/site2/plot/workshop.rs index 9ac64acb1f..dc06b9601d 100644 --- a/world/src/site2/plot/workshop.rs +++ b/world/src/site2/plot/workshop.rs @@ -128,6 +128,7 @@ impl Structure for Workshop { SpriteKind::Loom, SpriteKind::Anvil, SpriteKind::DismantlingBench, + SpriteKind::RepairBench, ]; 'outer: for d in 0..3 { for dir in CARDINALS {