diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e66c3154..bd41ea105a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add chest to each dungeon with unique loot - Added a new option in the graphics menu to enable GPU timing (not always supported). The timing values can be viewed in the HUD debug info (F3) and will be saved as chrome trace files in the working directory when taking a screenshot. - Added new Present Mode option in the graphics menu. Selecting Fifo (i.e. vsync) or Mailbox can be used to eliminate screen tearing. +- Quality color indicators next to recipe names in crafting menu ### Changed @@ -121,6 +122,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved the rest of screenshot work into the background. Screenshoting no longer induces large pauses. - Reworked tidal warrior to have unique attacks - Reworked yeti to have unique attacks +- Widened recipe name list in crafting menu ### Removed diff --git a/assets/voxygen/element/ui/crafting/crafting.png b/assets/voxygen/element/ui/crafting/crafting.png index b7ad8cb867..016de50f1c 100644 --- a/assets/voxygen/element/ui/crafting/crafting.png +++ b/assets/voxygen/element/ui/crafting/crafting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3beb25b03faf16a78b78703f2006781eca832c5ae408cca7dfe028a5295ba574 -size 2978 +oid sha256:3b02077a3e41af451a56b7e40915ee2741dc4f4a2d17ea6d8b8a69a6c8809c51 +size 8908 diff --git a/assets/voxygen/element/ui/crafting/crafting_frame.png b/assets/voxygen/element/ui/crafting/crafting_frame.png index 865a08a9bf..9d027a9397 100644 --- a/assets/voxygen/element/ui/crafting/crafting_frame.png +++ b/assets/voxygen/element/ui/crafting/crafting_frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00e47d30ea7f0578532b9dd218f37beb10ba94a36d76f950ea17c967ac8e62df -size 2932 +oid sha256:76b81760705df1f69043a252bf1d0104ca7b64a3860e5e500f33ca1c5db15cfc +size 6392 diff --git a/assets/voxygen/element/ui/crafting/quality_indicator.png b/assets/voxygen/element/ui/crafting/quality_indicator.png new file mode 100644 index 0000000000..8829b36e0e --- /dev/null +++ b/assets/voxygen/element/ui/crafting/quality_indicator.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53543ae3d5064ea83733135f8d2eaf673d77d535ab9ed939c827d00ff23de262 +size 612 diff --git a/voxygen/src/hud/crafting.rs b/voxygen/src/hud/crafting.rs index 3baeb66e60..761405f684 100644 --- a/voxygen/src/hud/crafting.rs +++ b/voxygen/src/hud/crafting.rs @@ -1,4 +1,5 @@ use super::{ + get_quality_col, img_ids::{Imgs, ImgsRot}, item_imgs::{animate_by_pulse, ItemImgs, ItemKey::Tool}, Show, TEXT_COLOR, TEXT_DULL_RED_COLOR, TEXT_GRAY_COLOR, UI_HIGHLIGHT_0, UI_MAIN, @@ -24,6 +25,7 @@ use common::{ }; use conrod_core::{ color, image, + position::Dimension, widget::{self, Button, Image, Rectangle, Scrollbar, Text, TextEdit}, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; @@ -52,7 +54,9 @@ widget_ids! { align_ing, scrollbar_ing, btn_craft, - recipe_names[], + recipe_list_btns[], + recipe_list_labels[], + recipe_list_quality_indicators[], recipe_img_frame[], recipe_img[], ingredients[], @@ -274,13 +278,13 @@ impl<'a> Widget for Crafting<'a> { 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) + .w_h(456.0, 460.0) .set(state.ids.window, ui); // Window Image::new(self.imgs.crafting_frame) .middle_of(state.ids.window) .color(Some(UI_HIGHLIGHT_0)) - .w_h(422.0, 460.0) + .wh_of(state.ids.window) .set(state.ids.window_frame, ui); // Crafting Icon @@ -310,7 +314,7 @@ impl<'a> Widget for Crafting<'a> { .set(state.ids.title_main, ui); // Alignment - Rectangle::fill_with([136.0, 378.0], color::TRANSPARENT) + Rectangle::fill_with([170.0, 378.0], color::TRANSPARENT) .top_left_with_margins_on(state.ids.window_frame, 74.0, 5.0) .scroll_kids_vertically() .set(state.ids.align_rec, ui); @@ -398,12 +402,6 @@ impl<'a> Widget for Crafting<'a> { // First available recipes, then unavailable ones, each alphabetically // In the triples, "name" is the recipe book key, and "recipe.output.0.name()" // is the display name (as stored in the item descriptors) - #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - enum RecipeIngredientQuantity { - All, - Some, - None, - } let mut ordered_recipes: Vec<_> = self .client .recipe_book() @@ -420,15 +418,7 @@ impl<'a> Widget for Crafting<'a> { } }) .map(|(name, recipe)| { - let at_least_some_ingredients = recipe.inputs.iter().any(|(input, amount)| { - *amount > 0 - && self.inventory.slots().any(|slot| { - slot.as_ref() - .map(|item| item.matches_recipe_input(input)) - .unwrap_or(false) - }) - }); - let show_craft_sprite = + let is_craftable = self.client .available_recipes() .get(name.as_str()) @@ -437,81 +427,112 @@ impl<'a> Widget for Crafting<'a> { Some(cs) == self.show.craft_sprite.map(|(_, s)| s) }) }); - let state = if show_craft_sprite { - RecipeIngredientQuantity::All - } else if at_least_some_ingredients { - RecipeIngredientQuantity::Some - } else { - RecipeIngredientQuantity::None - }; - (name, recipe, state) + (name, recipe, is_craftable) }) .collect(); - ordered_recipes.sort_by_key(|(_, recipe, state)| (*state, recipe.output.0.name())); + ordered_recipes.sort_by_key(|(_, recipe, state)| { + (!state, recipe.output.0.quality(), recipe.output.0.name()) + }); // Recipe list - if state.ids.recipe_names.len() < self.client.recipe_book().iter().len() { + if state.ids.recipe_list_btns.len() < self.client.recipe_book().iter().len() { state.update(|state| { - state.ids.recipe_names.resize( + state.ids.recipe_list_btns.resize( self.client.recipe_book().iter().len(), &mut ui.widget_id_generator(), ) }); } - for (i, (name, recipe, quantity)) in ordered_recipes + if state.ids.recipe_list_labels.len() < self.client.recipe_book().iter().len() { + state.update(|state| { + state.ids.recipe_list_labels.resize( + self.client.recipe_book().iter().len(), + &mut ui.widget_id_generator(), + ) + }); + } + if state.ids.recipe_list_quality_indicators.len() < self.client.recipe_book().iter().len() { + state.update(|state| { + state.ids.recipe_list_quality_indicators.resize( + self.client.recipe_book().iter().len(), + &mut ui.widget_id_generator(), + ) + }); + } + for (i, (name, recipe, is_craftable)) in ordered_recipes .into_iter() .filter(|(_, recipe, _)| self.show.crafting_tab.satisfies(recipe.output.0.as_ref())) .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) + let button = Button::image(if state.selected_recipe.as_ref() == Some(name) { + self.imgs.selection } else { - button.mid_bottom_with_margin_on(state.ids.recipe_names[i - 1], -25.0) - }; - let text_color = match quantity { - RecipeIngredientQuantity::All => TEXT_COLOR, - RecipeIngredientQuantity::Some => TEXT_GRAY_COLOR, - RecipeIngredientQuantity::None => TEXT_DULL_RED_COLOR, + self.imgs.nothing + }) + .and(|button| { + if i == 0 { + button.top_left_with_margins_on(state.ids.align_rec, 2.0, 7.0) + } else { + button.down_from(state.ids.recipe_list_btns[i - 1], 5.0) + } + }) + .w(157.0) + .hover_image(self.imgs.selection_hover) + .press_image(self.imgs.selection_press); + + let text = Text::new(recipe.output.0.name()) + .color(if is_craftable { + TEXT_COLOR + } else { + TEXT_GRAY_COLOR + }) + .font_size(self.fonts.cyri.scale(12)) + .font_id(self.fonts.cyri.conrod_id) + .w(149.0) + .mid_top_with_margin_on(state.ids.recipe_list_btns[i], 3.0) + .graphics_for(state.ids.recipe_list_btns[i]) + .center_justify(); + + let text_height = match text.get_y_dimension(ui) { + Dimension::Absolute(y) => y, + _ => 0.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(text_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) + .h((text_height + 7.0).max(20.0)) + .set(state.ids.recipe_list_btns[i], ui) .was_clicked() { - if state - .selected_recipe - .as_ref() - .map(|s| s == name) - .unwrap_or(false) - { + if state.selected_recipe.as_ref() == Some(name) { state.update(|s| s.selected_recipe = None); } else { state.update(|s| s.selected_recipe = Some(name.clone())); } } + // set the text here so that the correct position of the button is retrieved + text.set(state.ids.recipe_list_labels[i], ui); + + // Sidebar color + let color::Hsla(h, s, l, _) = get_quality_col(recipe.output.0.as_ref()).to_hsl(); + let val_multiplier = if is_craftable { 0.7 } else { 0.5 }; + // Apply conversion to hsv, multiply v by the desired amount, then revert to + // hsl. Conversion formulae: https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion + // Note that division by 0 is not possible since none of the colours are black + // or white + let quality_col = color::hsl( + h, + s * val_multiplier * f32::min(l, 1.0 - l) + / f32::min(l * val_multiplier, 1.0 - l * val_multiplier), + l * val_multiplier, + ); + + Button::image(self.imgs.quality_indicator) + .image_color(quality_col) + .h_of(state.ids.recipe_list_btns[i]) + .w(4.0) + .left_from(state.ids.recipe_list_btns[i], 1.0) + .graphics_for(state.ids.recipe_list_btns[i]) + .set(state.ids.recipe_list_quality_indicators[i], ui); } // Selected Recipe @@ -877,7 +898,7 @@ impl<'a> Widget for Crafting<'a> { { events.push(Event::SearchRecipe(None)); } - Rectangle::fill([114.0, 20.0]) + Rectangle::fill([148.0, 20.0]) .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 16.0) .hsla(0.0, 0.0, 0.0, 0.7) .depth(1.0) @@ -885,7 +906,7 @@ impl<'a> Widget for Crafting<'a> { .set(state.ids.input_bg_search, ui); if let Some(string) = TextEdit::new(key.as_str()) .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 18.0) - .w_h(90.0, 20.0) + .w_h(124.0, 20.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .color(TEXT_COLOR) @@ -902,7 +923,7 @@ impl<'a> Widget for Crafting<'a> { .color(TEXT_COLOR) .parent(state.ids.window) .set(state.ids.title_rec, ui); - Rectangle::fill_with([114.0, 20.0], color::TRANSPARENT) + Rectangle::fill_with([148.0, 20.0], color::TRANSPARENT) .top_left_with_margins_on(state.ids.window, 52.0, 26.0) .graphics_for(state.ids.btn_open_search) .set(state.ids.input_overlay_search, ui); diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 0277924e3d..3619f0b1cb 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -96,6 +96,7 @@ image_ids! { crafting_icon: "voxygen.element.ui.generic.buttons.anvil", crafting_icon_hover: "voxygen.element.ui.generic.buttons.anvil_hover", crafting_icon_press: "voxygen.element.ui.generic.buttons.anvil_press", + quality_indicator: "voxygen.element.ui.crafting.quality_indicator", icon_armor: "voxygen.element.ui.crafting.icons.armors", icon_tools: "voxygen.element.ui.crafting.icons.crafting_tools", icon_dismantle: "voxygen.element.ui.crafting.icons.dismantle", diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 7791d17629..a73bc4fc5a 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -135,7 +135,7 @@ const DEBUFF_COLOR: Color = Color::Rgba(0.79, 0.19, 0.17, 1.0); // Item Quality Colors const QUALITY_LOW: Color = Color::Rgba(0.41, 0.41, 0.41, 1.0); // Grey - Trash, can be sold to vendors -const QUALITY_COMMON: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0); // No Color - Crafting mats, food, starting equipment, quest items (like keys), rewards for easy quests +const QUALITY_COMMON: Color = Color::Rgba(0.79, 1.00, 1.00, 1.0); // No Color - Crafting mats, food, starting equipment, quest items (like keys), rewards for easy quests const QUALITY_MODERATE: Color = Color::Rgba(0.06, 0.69, 0.12, 1.0); // Green - Quest Rewards, commonly looted items from NPCs const QUALITY_HIGH: Color = Color::Rgba(0.18, 0.32, 0.9, 1.0); // Blue - Dungeon rewards, boss loot, rewards for hard quests const QUALITY_EPIC: Color = Color::Rgba(0.58, 0.29, 0.93, 1.0); // Purple - Rewards for epic quests and very hard bosses diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index 603cd80eb8..be0e92ccb8 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -19,6 +19,7 @@ pub use widgets::{ image_slider::ImageSlider, ingame::{Ingame, Ingameable}, item_tooltip::{ItemTooltip, ItemTooltipManager, ItemTooltipable}, + outlined_text::OutlinedText, radio_list::RadioList, slot, toggle_button::ToggleButton, diff --git a/voxygen/src/ui/widgets/mod.rs b/voxygen/src/ui/widgets/mod.rs index ca6ce261aa..e7ce5ee864 100644 --- a/voxygen/src/ui/widgets/mod.rs +++ b/voxygen/src/ui/widgets/mod.rs @@ -3,6 +3,7 @@ pub mod image_frame; pub mod image_slider; pub mod ingame; pub mod item_tooltip; +pub mod outlined_text; pub mod radio_list; pub mod slot; pub mod toggle_button; diff --git a/voxygen/src/ui/widgets/outlined_text.rs b/voxygen/src/ui/widgets/outlined_text.rs new file mode 100644 index 0000000000..8c778d5276 --- /dev/null +++ b/voxygen/src/ui/widgets/outlined_text.rs @@ -0,0 +1,117 @@ +use conrod_core::{ + builder_methods, text, + widget::{self, Text}, + widget_ids, Color, FontSize, Positionable, Sizeable, Widget, WidgetCommon, +}; + +#[derive(Clone, WidgetCommon)] +pub struct OutlinedText<'a> { + #[conrod(common_builder)] + common: widget::CommonBuilder, + + text: &'a str, + text_style: widget::text::Style, + outline_color: Option, + outline_width: f64, +} + +widget_ids! { + struct Ids{ + base, + outline1, + outline2, + outline3, + outline4, + } +} + +pub struct State { + ids: Ids, +} + +impl<'a> OutlinedText<'a> { + builder_methods! { + pub color {text_style.color = Some(Color)} + pub outline_color {outline_color = Some(Color)} + + pub font_size {text_style.font_size = Some(FontSize)} + pub outline_width {outline_width = f64} + } + + pub fn new(text: &'a str) -> Self { + Self { + common: widget::CommonBuilder::default(), + text, + + text_style: widget::text::Style::default(), + outline_color: None, + outline_width: 0.0, + } + } + + pub fn font_id(mut self, font_id: text::font::Id) -> Self { + self.text_style.font_id = Some(Some(font_id)); + self + } +} + +impl<'a> Widget for OutlinedText<'a> { + type Event = (); + type State = State; + type Style = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + #[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 { + id, + state, + ui, + rect, + .. + } = args; + + let mut outline_style = self.text_style; + outline_style.color = self.outline_color; + + let shift = self.outline_width; + Text::new(self.text) + .with_style(self.text_style) + .xy(rect.xy()) + .wh(rect.dim()) + .parent(id) + .depth(-1.0) + .set(state.ids.base, ui); + + Text::new(self.text) + .with_style(outline_style) + .x_y_relative_to(state.ids.base, shift, shift) + .wh_of(state.ids.base) + .set(state.ids.outline1, ui); + + Text::new(self.text) + .with_style(outline_style) + .x_y_relative_to(state.ids.base, -shift, shift) + .wh_of(state.ids.base) + .set(state.ids.outline2, ui); + + Text::new(self.text) + .with_style(outline_style) + .x_y_relative_to(state.ids.base, shift, -shift) + .wh_of(state.ids.base) + .set(state.ids.outline3, ui); + + Text::new(self.text) + .with_style(outline_style) + .x_y_relative_to(state.ids.base, -shift, -shift) + .wh_of(state.ids.base) + .set(state.ids.outline4, ui); + } +}