Equipment can now be repaired at sprites in town.

This commit is contained in:
Sam 2022-06-18 17:38:46 -04:00
parent a555e08d0b
commit c3f5bc13f1
16 changed files with 226 additions and 72 deletions

View File

@ -26,12 +26,19 @@ hud-crafting-tabs-utility = Utility
hud-crafting-tabs-weapon = Weapons hud-crafting-tabs-weapon = Weapons
hud-crafting-tabs-bag = Bags hud-crafting-tabs-bag = Bags
hud-crafting-tabs-processed_material = Materials hud-crafting-tabs-processed_material = Materials
hud-crafting-tabs-repair = Repair
hud-crafting-dismantle_title = Dismantling hud-crafting-dismantle_title = Dismantling
hud-crafting-dismantle_explanation = hud-crafting-dismantle_explanation =
Hover items in your bag to see what Hover items in your bag to see what
you can salvage. you can salvage.
Double-Click them to start dismantling. 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-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_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). hud-crafting-mod_weap_prim_slot_desc = Place a primary weapon component here (e.g. a sword blade, axe head, or bow limbs).

View File

@ -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<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 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) { fn update_available_recipes(&mut self) {
self.available_recipes = self self.available_recipes = self
.recipe_book .recipe_book

View File

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

View File

@ -1244,6 +1244,8 @@ impl Item {
} }
} }
pub fn reset_durability(&mut self) { self.durability = self.durability.map(|_| 0); }
#[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();

View File

@ -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)] #[cfg(test)]

View File

@ -887,6 +887,20 @@ impl Inventory {
) { ) {
self.loadout.apply_durability(ability_map, msm) 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 { impl Component for Inventory {

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

@ -856,6 +856,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
None 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 // Attempt to insert items into inventory, dropping them if there is not enough

View File

@ -86,7 +86,9 @@ widget_ids! {
dismantle_title, dismantle_title,
dismantle_img, dismantle_img,
dismantle_txt, dismantle_txt,
dismantle_highlight_txt, repair_title,
repair_img,
repair_txt,
modular_inputs[], modular_inputs[],
modular_art, modular_art,
modular_desc_txt, modular_desc_txt,
@ -122,6 +124,7 @@ 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 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, InvSlotId>,
} }
@ -133,6 +136,7 @@ impl Default for CraftingShow {
crafting_search_key: None, crafting_search_key: None,
craft_sprite: None, craft_sprite: None,
salvage: false, salvage: false,
repair: false,
recipe_inputs: HashMap::new(), recipe_inputs: HashMap::new(),
} }
} }
@ -207,7 +211,8 @@ pub enum CraftingTab {
Bag, Bag,
Utility, Utility,
Glider, Glider,
Dismantle, // Needs to be the last one or widget alignment will be messed up Dismantle,
Repair,
} }
impl CraftingTab { impl CraftingTab {
@ -224,6 +229,7 @@ impl CraftingTab {
CraftingTab::Bag => "hud-crafting-tabs-bag", CraftingTab::Bag => "hud-crafting-tabs-bag",
CraftingTab::ProcessedMaterial => "hud-crafting-tabs-processed_material", CraftingTab::ProcessedMaterial => "hud-crafting-tabs-processed_material",
CraftingTab::Dismantle => "hud-crafting-tabs-dismantle", CraftingTab::Dismantle => "hud-crafting-tabs-dismantle",
CraftingTab::Repair => "hud-crafting-tabs-repair",
} }
} }
@ -239,14 +245,16 @@ 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,
CraftingTab::Repair => imgs.not_found,
} }
} }
fn satisfies(self, recipe: &Recipe) -> bool { fn satisfies(self, recipe: &Recipe) -> bool {
let (item, _count) = &recipe.output; let (item, _count) = &recipe.output;
match self { match self {
CraftingTab::All | CraftingTab::Dismantle => true, CraftingTab::All | CraftingTab::Dismantle | CraftingTab::Repair => true,
CraftingTab::Food => item.tags().contains(&ItemTag::Food), CraftingTab::Food => item.tags().contains(&ItemTag::Food),
CraftingTab::Armor => match &*item.kind() { CraftingTab::Armor => match &*item.kind() {
ItemKind::Armor(_) => !item.tags().contains(&ItemTag::Bag), 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 { pub struct State {
@ -434,8 +446,10 @@ 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())
.enumerate()
{
let tab_img = crafting_tab.img_id(self.imgs); let tab_img = crafting_tab.img_id(self.imgs);
// Button Background // Button Background
let mut bg = Image::new(self.imgs.pixel) let mut bg = Image::new(self.imgs.pixel)
@ -484,7 +498,6 @@ impl<'a> Widget for Crafting<'a> {
.graphics_for(state.ids.category_tabs[i]) .graphics_for(state.ids.category_tabs[i])
.set(state.ids.category_imgs[i], ui); .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
// more filters gets added // more filters gets added
@ -735,12 +748,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,12 +812,9 @@ 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);
} }
@ -1854,6 +1861,42 @@ impl<'a> Widget for Crafting<'a> {
.color(TEXT_COLOR) .color(TEXT_COLOR)
.parent(state.ids.window) .parent(state.ids.window)
.set(state.ids.dismantle_txt, ui); .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 // Search / Title Recipes

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 {
slot: Slot,
sprite_pos: Vec3<i32>,
},
InviteMember(Uid), InviteMember(Uid),
AcceptInvite, AcceptInvite,
DeclineInvite, DeclineInvite,
@ -914,6 +922,8 @@ impl Show {
self.bag = open; self.bag = open;
self.map = false; self.map = false;
self.crafting_fields.salvage = false; self.crafting_fields.salvage = false;
self.crafting_fields.repair = false;
if !open { if !open {
self.crafting = false; self.crafting = false;
} }
@ -937,6 +947,7 @@ impl Show {
self.bag = false; self.bag = false;
self.crafting = false; self.crafting = false;
self.crafting_fields.salvage = false; self.crafting_fields.salvage = false;
self.crafting_fields.repair = false;
self.social = false; self.social = false;
self.quest = false; self.quest = false;
self.diary = false; self.diary = false;
@ -973,6 +984,7 @@ impl Show {
} }
self.crafting = open; self.crafting = open;
self.crafting_fields.salvage = false; self.crafting_fields.salvage = false;
self.crafting_fields.repair = false;
self.crafting_fields.recipe_inputs = HashMap::new(); self.crafting_fields.recipe_inputs = HashMap::new();
self.bag = open; self.bag = open;
self.map = false; self.map = false;
@ -992,6 +1004,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.repair = matches!(
self.crafting_fields.craft_sprite,
Some((_, SpriteKind::RepairBench))
) && matches!(tab, CraftingTab::Repair);
} }
fn diary(&mut self, open: bool) { fn diary(&mut self, open: bool) {
@ -1000,6 +1016,7 @@ impl Show {
self.quest = false; self.quest = false;
self.crafting = false; self.crafting = false;
self.crafting_fields.salvage = false; self.crafting_fields.salvage = false;
self.crafting_fields.repair = false;
self.bag = false; self.bag = false;
self.map = false; self.map = false;
self.diary_fields = diary::DiaryShow::default(); self.diary_fields = diary::DiaryShow::default();
@ -1020,6 +1037,7 @@ impl Show {
self.quest = false; self.quest = false;
self.crafting = false; self.crafting = false;
self.crafting_fields.salvage = false; self.crafting_fields.salvage = false;
self.crafting_fields.repair = false;
self.diary = false; self.diary = false;
self.want_grab = !self.any_window_requires_cursor(); self.want_grab = !self.any_window_requires_cursor();
} }
@ -3688,7 +3706,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 {
@ -3920,6 +3937,17 @@ impl Hud {
{ {
events.push(Event::SalvageItem { slot, salvage_pos }) 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 { } else {
events.push(Event::UseSlot { events.push(Event::UseSlot {
slot: from, slot: from,

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::Repair)))
},
_ => {}, _ => {},
}, },
} }

View File

@ -1777,6 +1777,9 @@ 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 { slot, sprite_pos } => {
self.client.borrow_mut().repair_item(slot, 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

@ -675,7 +675,7 @@ impl<'a> Widget for ItemTooltip<'a> {
let durability = MAX_DURABILITY - durability.min(MAX_DURABILITY); let durability = MAX_DURABILITY - durability.min(MAX_DURABILITY);
widget::Text::new(&format!( widget::Text::new(&format!(
"{} : {}/{}", "{} : {}/{}",
i18n.get("common.stats.durability"), i18n.get_msg("common-stats-durability"),
durability, durability,
MAX_DURABILITY MAX_DURABILITY
)) ))
@ -900,7 +900,7 @@ impl<'a> Widget for ItemTooltip<'a> {
stat_text( stat_text(
format!( format!(
"{} : {}/{}", "{} : {}/{}",
i18n.get("common.stats.durability"), i18n.get_msg("common-stats-durability"),
durability, durability,
Item::MAX_DURABILITY Item::MAX_DURABILITY
), ),

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

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