use super::{ img_ids::{Imgs, ImgsRot}, item_imgs::ItemImgs, TEXT_COLOR, TEXT_DULL_RED_COLOR, TEXT_GRAY_COLOR, UI_HIGHLIGHT_0, UI_MAIN, }; use crate::{ hud::get_quality_col, i18n::VoxygenLocalization, ui::{fonts::ConrodVoxygenFonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, }; use client::{self, Client}; use common::comp::{ item::{ItemDesc, Quality}, Inventory, }; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; widget_ids! { pub struct Ids { window, window_frame, close, icon, title_main, title_rec, align_rec, scrollbar_rec, title_ing, align_ing, scrollbar_ing, btn_craft, recipe_names[], recipe_img_frame[], recipe_img[], ingredients[], ingredient_frame[], ingredient_img[], req_text[], ingredients_txt, output_img_frame, output_img, output_amount, } } pub enum Event { CraftRecipe(String), Close, } #[derive(WidgetCommon)] pub struct Crafting<'a> { client: &'a Client, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, item_imgs: &'a ItemImgs, inventory: &'a Inventory, #[conrod(common_builder)] common: widget::CommonBuilder, } #[allow(clippy::too_many_arguments)] impl<'a> Crafting<'a> { pub fn new( client: &'a Client, imgs: &'a Imgs, fonts: &'a ConrodVoxygenFonts, localized_strings: &'a std::sync::Arc, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, item_imgs: &'a ItemImgs, inventory: &'a Inventory, ) -> Self { Self { client, imgs, fonts, localized_strings, rot_imgs, tooltip_manager, item_imgs, inventory, common: widget::CommonBuilder::default(), } } } pub struct State { ids: Ids, selected_recipe: Option, } impl<'a> Widget for Crafting<'a> { type Event = Vec; type State = State; type Style = (); fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { State { ids: Ids::new(id_gen), selected_recipe: None, } } #[allow(clippy::unused_unit)] // TODO: Pending review in #587 fn style(&self) -> Self::Style { () } fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; if state.ids.recipe_names.len() < self.client.recipe_book().iter().len() { state.update(|state| { state.ids.recipe_names.resize( self.client.recipe_book().iter().len(), &mut ui.widget_id_generator(), ) }); } let ids = &state.ids; let mut events = Vec::new(); // Tooltips let item_tooltip = Tooltip::new({ let edge = &self.rot_imgs.tt_side; let corner = &self.rot_imgs.tt_corner; ImageFrame::new( [edge.cw180, edge.none, edge.cw270, edge.cw90], [corner.none, corner.cw270, corner.cw90, corner.cw180], Color::Rgba(0.08, 0.07, 0.04, 1.0), 5.0, ) }) .title_font_size(self.fonts.cyri.scale(15)) .parent(ui.window) .desc_font_size(self.fonts.cyri.scale(12)) .font_id(self.fonts.cyri.conrod_id) .desc_text_color(TEXT_COLOR); Image::new(self.imgs.crafting_window) .bottom_right_with_margins_on(ui.window, 308.0, 450.0) .color(Some(UI_MAIN)) .w_h(422.0, 460.0) .set(ids.window, ui); Image::new(self.imgs.crafting_frame) .middle_of(ids.window) .color(Some(UI_HIGHLIGHT_0)) .w_h(422.0, 460.0) .set(ids.window_frame, ui); Image::new(self.imgs.crafting_icon_bordered) .w_h(38.0, 38.0) .top_left_with_margins_on(state.ids.window_frame, 4.0, 4.0) .set(state.ids.icon, ui); // Close Button if Button::image(self.imgs.close_button) .w_h(24.0, 25.0) .hover_image(self.imgs.close_button_hover) .press_image(self.imgs.close_button_press) .top_right_with_margins_on(ids.window, 0.0, 0.0) .set(ids.close, ui) .was_clicked() { events.push(Event::Close); } // Title Text::new(&self.localized_strings.get("hud.crafting")) .mid_top_with_margin_on(ids.window_frame, 9.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(20)) .color(TEXT_COLOR) .set(ids.title_main, ui); // Alignment Rectangle::fill_with([136.0, 378.0], color::TRANSPARENT) .top_left_with_margins_on(ids.window_frame, 74.0, 5.0) .scroll_kids_vertically() .set(ids.align_rec, ui); Rectangle::fill_with([274.0, 340.0], color::TRANSPARENT) .top_right_with_margins_on(ids.window, 74.0, 5.0) .set(ids.align_ing, ui); let client = &self.client; // First available recipes, then unavailable ones // TODO Sort these alphabetically by using "sort_by_key(|x| x.to_lowercase())" let recipe_iter = self .client .recipe_book() .iter() .filter(|(name, _)| client.available_recipes().contains(name.as_str())) .map(|(name, recipe)| (name, recipe, true)) .chain( client .recipe_book() .iter() .filter(|(name, _)| !client.available_recipes().contains(name.as_str())) .map(|(name, recipe)| (name, recipe, false)), ); match &state.selected_recipe { None => {}, Some(recipe) => { let can_perform = client.available_recipes().contains(recipe.as_str()); // Ingredients Text Text::new(&self.localized_strings.get("hud.crafting.ingredients")) .top_left_with_margins_on(state.ids.align_ing, 10.0, 5.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(18)) .color(TEXT_COLOR) .set(state.ids.ingredients_txt, ui); // Craft button if Button::image(self.imgs.button) .w_h(105.0, 25.0) .hover_image( can_perform .then_some(self.imgs.button_hover) .unwrap_or(self.imgs.button), ) .press_image( can_perform .then_some(self.imgs.button_press) .unwrap_or(self.imgs.button), ) .label(&self.localized_strings.get("hud.crafting.craft")) .label_y(conrod_core::position::Relative::Scalar(1.0)) .label_color(can_perform.then_some(TEXT_COLOR).unwrap_or(TEXT_GRAY_COLOR)) .label_font_size(self.fonts.cyri.scale(12)) .label_font_id(self.fonts.cyri.conrod_id) .image_color(can_perform.then_some(TEXT_COLOR).unwrap_or(TEXT_GRAY_COLOR)) .mid_bottom_with_margin_on(ids.align_ing, -31.0) .set(ids.btn_craft, ui) .was_clicked() { events.push(Event::CraftRecipe(recipe.clone())); } // Result Image BG let quality_col_img = if let Some(recipe) = state .selected_recipe .as_ref() .and_then(|r| self.client.recipe_book().get(r.as_str())) { match recipe.output.0.quality { Quality::Low => self.imgs.inv_slot_grey, Quality::Common => self.imgs.inv_slot, Quality::Moderate => self.imgs.inv_slot_green, Quality::High => self.imgs.inv_slot_blue, Quality::Epic => self.imgs.inv_slot_purple, Quality::Legendary => self.imgs.inv_slot_gold, Quality::Artifact => self.imgs.inv_slot_orange, _ => self.imgs.inv_slot_red, } } else { self.imgs.inv_slot }; Image::new(quality_col_img) .w_h(60.0, 60.0) .top_right_with_margins_on(state.ids.align_ing, 15.0, 10.0) .parent(ids.align_ing) .set(ids.output_img_frame, ui); if let Some(recipe) = state .selected_recipe .as_ref() .and_then(|r| self.client.recipe_book().get(r.as_str())) { let output_text = format!("x{}", &recipe.output.1.to_string()); // Output Image let (title, desc) = super::util::item_text(&*recipe.output.0); let quality_col = get_quality_col(&*recipe.output.0); Button::image( self.item_imgs .img_id_or_not_found_img((&*recipe.output.0.kind()).into()), ) .w_h(55.0, 55.0) .label(&output_text) .label_color(TEXT_COLOR) .label_font_size(self.fonts.cyri.scale(14)) .label_font_id(self.fonts.cyri.conrod_id) .label_y(conrod_core::position::Relative::Scalar(-24.0)) .label_x(conrod_core::position::Relative::Scalar(24.0)) .middle_of(state.ids.output_img_frame) .with_tooltip( self.tooltip_manager, title, &*desc, &item_tooltip, quality_col, ) .set(state.ids.output_img, ui); } }, } // Recipe list for (i, (name, recipe, can_perform)) in recipe_iter.enumerate() { let button = Button::image( if state .selected_recipe .as_ref() .map(|s| s != name) .unwrap_or(false) { self.imgs.nothing } else { match state.selected_recipe { None => self.imgs.nothing, Some(_) => self.imgs.selection, } }, ); // Recipe Button let button = if i == 0 { button.mid_top_with_margin_on(state.ids.align_rec, 2.0) } else { button.mid_bottom_with_margin_on(state.ids.recipe_names[i - 1], -25.0) }; if button .label(recipe.output.0.name()) .w_h(130.0, 20.0) .hover_image(self.imgs.selection_hover) .press_image(self.imgs.selection_press) .label_color(can_perform.then_some(TEXT_COLOR).unwrap_or(TEXT_GRAY_COLOR)) .label_font_size(self.fonts.cyri.scale(12)) .label_font_id(self.fonts.cyri.conrod_id) .label_y(conrod_core::position::Relative::Scalar(2.0)) .set(state.ids.recipe_names[i], ui) .was_clicked() { if state .selected_recipe .as_ref() .map(|s| s == name) .unwrap_or(false) { state.update(|s| s.selected_recipe = None); } else { state.update(|s| s.selected_recipe = Some(name.clone())); } } } //Ingredients if let Some(recipe) = state .selected_recipe .as_ref() .and_then(|r| self.client.recipe_book().get(r.as_str())) { // Title Text::new(&recipe.output.0.name()) .mid_top_with_margin_on(state.ids.align_ing, -22.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.title_ing, ui); // Ingredient images with tooltip if state.ids.ingredient_frame.len() < recipe.inputs().len() { state.update(|state| { state .ids .ingredient_frame .resize(recipe.inputs().len(), &mut ui.widget_id_generator()) }); }; if state.ids.ingredients.len() < recipe.inputs().len() { state.update(|state| { state .ids .ingredients .resize(recipe.inputs().len(), &mut ui.widget_id_generator()) }); }; if state.ids.ingredient_img.len() < recipe.inputs().len() { state.update(|state| { state .ids .ingredient_img .resize(recipe.inputs().len(), &mut ui.widget_id_generator()) }); }; if state.ids.req_text.len() < recipe.inputs().len() { state.update(|state| { state .ids .req_text .resize(recipe.inputs().len(), &mut ui.widget_id_generator()) }); }; // Widget generation for every ingredient for (i, (item_def, amount)) in recipe.inputs.iter().enumerate() { // Grey color for images and text if their amount is too low to craft the item let item_count_in_inventory = self.inventory.item_count(item_def); let col = if item_count_in_inventory >= u64::from(*amount) && *amount != 0 { TEXT_COLOR } else { TEXT_DULL_RED_COLOR }; // Slot BG let frame_pos = if i == 0 { state.ids.ingredients_txt } else { state.ids.ingredient_frame[i - 1] }; // add a larger offset for the the first ingredient and the "Required Text for // Catalysts/Tools" let frame_offset = if i == 0 { 10.0 } else if *amount == 0 { 5.0 } else { 0.0 }; let quality_col = get_quality_col(&**item_def); let quality_col_img = match &item_def.quality { Quality::Low => self.imgs.inv_slot_grey, Quality::Common => self.imgs.inv_slot, Quality::Moderate => self.imgs.inv_slot_green, Quality::High => self.imgs.inv_slot_blue, Quality::Epic => self.imgs.inv_slot_purple, Quality::Legendary => self.imgs.inv_slot_gold, Quality::Artifact => self.imgs.inv_slot_orange, _ => self.imgs.inv_slot_red, }; let frame = Image::new(quality_col_img).w_h(25.0, 25.0); let frame = if *amount == 0 { frame.down_from(state.ids.req_text[i], 10.0 + frame_offset) } else { frame.down_from(frame_pos, 10.0 + frame_offset) }; frame.set(state.ids.ingredient_frame[i], ui); //Item Image let (title, desc) = super::util::item_text(&**item_def); Button::image( self.item_imgs .img_id_or_not_found_img((&*item_def.kind()).into()), ) .w_h(22.0, 22.0) .middle_of(state.ids.ingredient_frame[i]) .with_tooltip( self.tooltip_manager, title, &*desc, &item_tooltip, quality_col, ) .set(state.ids.ingredient_img[i], ui); // Ingredients text and amount // Don't show inventory amounts above 999 to avoid the widget clipping let over9k = "99+"; let in_inv: &str = &item_count_in_inventory.to_string(); // Show Ingredients // Align "Required" Text below last ingredient if *amount == 0 { // Catalysts/Tools Text::new(&self.localized_strings.get("hud.crafting.tool_cata")) .down_from(state.ids.ingredient_frame[i - 1], 20.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .color(TEXT_COLOR) .set(state.ids.req_text[i], ui); Text::new(&item_def.name()) .right_from(state.ids.ingredient_frame[i], 10.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .color(col) .set(state.ids.ingredients[i], ui); } else { // Ingredients let input = format!( "{}x {} ({})", amount, &item_def.name(), if item_count_in_inventory > 99 { over9k } else { in_inv } ); // Ingredient Text Text::new(&input) .right_from(state.ids.ingredient_frame[i], 10.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(12)) .color(col) .set(state.ids.ingredients[i], ui); } } } let ids = &state.ids; // Scrollbars Scrollbar::y_axis(ids.align_rec) .thickness(5.0) .rgba(0.33, 0.33, 0.33, 1.0) .set(ids.scrollbar_rec, ui); Scrollbar::y_axis(ids.align_ing) .thickness(5.0) .rgba(0.33, 0.33, 0.33, 1.0) .set(ids.scrollbar_ing, ui); // Title Recipes and Ingredients Text::new(&self.localized_strings.get("hud.crafting.recipes")) .mid_top_with_margin_on(ids.align_rec, -22.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .color(TEXT_COLOR) .parent(ids.window) .set(ids.title_rec, ui); events } }