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-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).

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) {
self.available_recipes = self
.recipe_book

View File

@ -105,6 +105,7 @@ pub enum CraftEvent {
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
},
Repair(Slot),
}
#[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)]
pub fn create_test_item_from_kind(kind: ItemKind) -> Self {
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)]

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>,
pub craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
pub salvage: bool,
pub repair: bool,
// TODO: Maybe try to do something that doesn't need to allocate?
pub recipe_inputs: HashMap<u32, InvSlotId>,
}
@ -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

View File

@ -87,7 +87,11 @@ use common::{
self,
ability::{AuxiliaryAbility, Stance},
fluid_dynamics,
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
inventory::{
slot::{InvSlotId, Slot},
trade_pricing::TradePricing,
CollectFailedReason,
},
item::{
tool::{AbilityContext, ToolKind},
ItemDesc, MaterialStatManifest, Quality,
@ -721,6 +725,10 @@ pub enum Event {
modifier: Option<InvSlotId>,
craft_sprite: Option<Vec3<i32>>,
},
RepairItem {
slot: Slot,
sprite_pos: Vec3<i32>,
},
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,

View File

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

View File

@ -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);
},

View File

@ -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
),

View File

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

View File

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