1use super::{
2 HudInfo, Show, TEXT_COLOR, TEXT_DULL_RED_COLOR, TEXT_GRAY_COLOR, UI_HIGHLIGHT_0, UI_MAIN,
3 get_quality_col,
4 img_ids::{Imgs, ImgsRot},
5 item_imgs::{ItemImgs, animate_by_pulse},
6 slots::{CraftSlot, CraftSlotInfo, SlotManager},
7 util,
8};
9use crate::{
10 GlobalState,
11 settings::{HudPositionSettings, Settings},
12 ui::{
13 ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, Tooltip, TooltipManager,
14 Tooltipable,
15 fonts::Fonts,
16 slot::{ContentSize, SlotMaker},
17 },
18};
19use client::{self, Client};
20use common::{
21 assets::AssetExt,
22 comp::inventory::{
23 Inventory,
24 item::{
25 Item, ItemBase, ItemDef, ItemDesc, ItemI18n, ItemKind, ItemTag, MaterialStatManifest,
26 Quality, TagExampleInfo,
27 item_key::ItemKey,
28 modular::{self, ModularComponent},
29 tool::{AbilityMap, ToolKind},
30 },
31 slot::{InvSlotId, Slot},
32 },
33 mounting::VolumePos,
34 recipe::{ComponentKey, Recipe, RecipeBookManifest, RecipeInput},
35 terrain::SpriteKind,
36};
37use conrod_core::{
38 Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, color, image,
39 position::{Dimension, Place},
40 widget::{self, Button, Image, Line, Rectangle, Scrollbar, Text, TextEdit},
41 widget_ids,
42};
43use hashbrown::{HashMap, HashSet};
44use i18n::Localization;
45use itertools::Either;
46use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
47use strum::{EnumIter, IntoEnumIterator};
48use tracing::{error, warn};
49use vek::{approx::AbsDiffEq, *};
50
51widget_ids! {
52 pub struct Ids {
53 window,
54 window_frame,
55 close,
56 icon,
57 title_main,
58 title_rec,
59 align_rec,
60 scrollbar_rec,
61 btn_show_all_recipes,
62 btn_open_search,
63 btn_close_search,
64 input_search,
65 input_bg_search,
66 input_overlay_search,
67 title_ing,
68 tags_ing[],
69 align_ing,
70 scrollbar_ing,
71 btn_separator,
72 btn_craft,
73 btn_craft_all,
74 recipe_list_btns[],
75 recipe_list_labels[],
76 recipe_list_quality_indicators[],
77 recipe_list_materials_indicators[],
78 ingredients[],
79 ingredient_frame[],
80 ingredient_btn[],
81 ingredient_img[],
82 req_text[],
83 ingredients_txt,
84 req_station_title,
85 req_station_img,
86 req_station_txt,
87 output_img_frame,
88 output_img,
89 category_bgs[],
90 category_tabs[],
91 category_imgs[],
92 dismantle_title,
93 dismantle_img,
94 dismantle_txt,
95 repair_buttons[],
96 craft_slots[],
97 modular_art,
98 modular_desc_txt,
99 modular_wep_empty_bg,
100 modular_wep_ing_1_bg,
101 modular_wep_ing_2_bg,
102 draggable_area,
103 }
104}
105
106pub enum Event {
107 CraftRecipe {
108 recipe_name: String,
109 amount: u32,
110 },
111 CraftModularWeapon {
112 primary_slot: InvSlotId,
113 secondary_slot: InvSlotId,
114 },
115 CraftModularWeaponComponent {
116 toolkind: ToolKind,
117 material: InvSlotId,
118 modifier: Option<InvSlotId>,
119 },
120 ChangeCraftingTab(CraftingTab),
121 Close,
122 Focus(widget::Id),
123 SearchRecipe(Option<String>),
124 ShowAllRecipes(bool),
125 ClearRecipeInputs,
126 RepairItem {
127 slot: Slot,
128 },
129 MoveCrafting(Vec2<f64>),
130}
131
132pub struct CraftingShow {
133 pub crafting_tab: CraftingTab,
134 pub crafting_search_key: Option<String>,
135 pub craft_sprite: Option<(VolumePos, SpriteKind)>,
136 pub salvage: bool,
137 pub initialize_repair: bool,
138 pub recipe_inputs: HashMap<u32, Slot>,
140}
141
142impl Default for CraftingShow {
143 fn default() -> Self {
144 Self {
145 crafting_tab: CraftingTab::All,
146 crafting_search_key: None,
147 craft_sprite: None,
148 salvage: false,
149 initialize_repair: false,
150 recipe_inputs: HashMap::new(),
151 }
152 }
153}
154
155#[derive(WidgetCommon)]
156pub struct Crafting<'a> {
157 client: &'a Client,
158 global_state: &'a GlobalState,
159 info: &'a HudInfo<'a>,
160 imgs: &'a Imgs,
161 fonts: &'a Fonts,
162 localized_strings: &'a Localization,
163 item_i18n: &'a ItemI18n,
164 pulse: f32,
165 rot_imgs: &'a ImgsRot,
166 item_tooltip_manager: &'a mut ItemTooltipManager,
167 slot_manager: &'a mut SlotManager,
168 item_imgs: &'a ItemImgs,
169 inventory: &'a Inventory,
170 rbm: &'a RecipeBookManifest,
171 msm: &'a MaterialStatManifest,
172 #[conrod(common_builder)]
173 common: widget::CommonBuilder,
174 tooltip_manager: &'a mut TooltipManager,
175 show: &'a mut Show,
176 settings: &'a Settings,
177}
178
179impl<'a> Crafting<'a> {
180 #[expect(clippy::too_many_arguments)]
181 pub fn new(
182 client: &'a Client,
183 global_state: &'a GlobalState,
184 info: &'a HudInfo,
185 imgs: &'a Imgs,
186 fonts: &'a Fonts,
187 localized_strings: &'a Localization,
188 item_i18n: &'a ItemI18n,
189 pulse: f32,
190 rot_imgs: &'a ImgsRot,
191 item_tooltip_manager: &'a mut ItemTooltipManager,
192 slot_manager: &'a mut SlotManager,
193 item_imgs: &'a ItemImgs,
194 inventory: &'a Inventory,
195 rbm: &'a RecipeBookManifest,
196 msm: &'a MaterialStatManifest,
197 tooltip_manager: &'a mut TooltipManager,
198 show: &'a mut Show,
199 settings: &'a Settings,
200 ) -> Self {
201 Self {
202 client,
203 global_state,
204 info,
205 imgs,
206 fonts,
207 localized_strings,
208 item_i18n,
209 pulse,
210 rot_imgs,
211 item_tooltip_manager,
212 slot_manager,
213 tooltip_manager,
214 item_imgs,
215 inventory,
216 rbm,
217 msm,
218 show,
219 common: widget::CommonBuilder::default(),
220 settings,
221 }
222 }
223}
224
225#[derive(Copy, Clone, Debug, EnumIter, PartialEq, Eq)]
226pub enum CraftingTab {
227 All,
228 Tool,
229 Armor,
230 Weapon,
231 ProcessedMaterial,
232 Food,
233 Potion,
234 Bag,
235 Utility,
236 Glider,
237 Dismantle,
238}
239
240impl CraftingTab {
241 fn name_key(self) -> &'static str {
242 match self {
243 CraftingTab::All => "hud-crafting-tabs-all",
244 CraftingTab::Armor => "hud-crafting-tabs-armor",
245 CraftingTab::Food => "hud-crafting-tabs-food",
246 CraftingTab::Glider => "hud-crafting-tabs-glider",
247 CraftingTab::Potion => "hud-crafting-tabs-potion",
248 CraftingTab::Tool => "hud-crafting-tabs-tool",
249 CraftingTab::Utility => "hud-crafting-tabs-utility",
250 CraftingTab::Weapon => "hud-crafting-tabs-weapon",
251 CraftingTab::Bag => "hud-crafting-tabs-bag",
252 CraftingTab::ProcessedMaterial => "hud-crafting-tabs-processed_material",
253 CraftingTab::Dismantle => "hud-crafting-tabs-dismantle",
254 }
255 }
256
257 fn img_id(self, imgs: &Imgs) -> image::Id {
258 match self {
259 CraftingTab::All => imgs.icon_globe,
260 CraftingTab::Armor => imgs.icon_armor,
261 CraftingTab::Food => imgs.icon_food,
262 CraftingTab::Glider => imgs.icon_glider,
263 CraftingTab::Potion => imgs.icon_potion,
264 CraftingTab::Tool => imgs.icon_tools,
265 CraftingTab::Utility => imgs.icon_utility,
266 CraftingTab::Weapon => imgs.icon_weapon,
267 CraftingTab::Bag => imgs.icon_bag,
268 CraftingTab::ProcessedMaterial => imgs.icon_processed_material,
269 CraftingTab::Dismantle => imgs.not_found,
271 }
272 }
273
274 fn satisfies(self, recipe: &Recipe) -> bool {
275 let (item, _count) = &recipe.output;
276 match self {
277 CraftingTab::All | CraftingTab::Dismantle => true,
278 CraftingTab::Food => item.tags().contains(&ItemTag::Food),
279 CraftingTab::Armor => match &*item.kind() {
280 ItemKind::Armor(_) => !item.tags().contains(&ItemTag::Bag),
281 _ => false,
282 },
283 CraftingTab::Glider => matches!(&*item.kind(), ItemKind::Glider),
284 CraftingTab::Potion => item
285 .tags()
286 .iter()
287 .any(|tag| matches!(tag, &ItemTag::Potion | &ItemTag::Charm)),
288 CraftingTab::ProcessedMaterial => item
289 .tags()
290 .iter()
291 .any(|tag| matches!(tag, &ItemTag::MaterialKind(_) | &ItemTag::BaseMaterial)),
292 CraftingTab::Bag => item.tags().contains(&ItemTag::Bag),
293 CraftingTab::Tool => item.tags().contains(&ItemTag::CraftingTool),
294 CraftingTab::Utility => item.tags().contains(&ItemTag::Utility),
295 CraftingTab::Weapon => match &*item.kind() {
296 ItemKind::Tool(_) => !item.tags().contains(&ItemTag::CraftingTool),
297 ItemKind::ModularComponent(
298 ModularComponent::ToolPrimaryComponent { .. }
299 | ModularComponent::ToolSecondaryComponent { .. },
300 ) => true,
301 _ => false,
302 },
303 }
304 }
305
306 fn is_adhoc(self) -> bool { matches!(self, CraftingTab::Dismantle) }
309}
310
311pub struct State {
312 ids: Ids,
313 selected_recipe: Option<String>,
314}
315
316enum SearchFilter {
317 None,
318 Input,
319 Nonexistent,
320}
321
322impl SearchFilter {
323 fn parse_from_str(string: &str) -> Self {
324 match string {
325 "input" => Self::Input,
326 _ => Self::Nonexistent,
327 }
328 }
329}
330
331impl Widget for Crafting<'_> {
332 type Event = Vec<Event>;
333 type State = State;
334 type Style = ();
335
336 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
337 State {
338 ids: Ids::new(id_gen),
339 selected_recipe: None,
340 }
341 }
342
343 fn style(&self) -> Self::Style {}
344
345 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
346 common_base::prof_span!("Crafting::update");
347 let widget::UpdateArgs { state, ui, .. } = args;
348
349 let mut events = Vec::new();
350
351 if self.show.crafting_fields.initialize_repair {
355 state.update(|s| {
356 s.selected_recipe = Some(String::from("veloren.core.pseudo_recipe.repair"))
357 });
358 }
359 self.show.crafting_fields.initialize_repair = false;
360
361 let item_tooltip = ItemTooltip::new(
363 {
364 let edge = &self.rot_imgs.tt_side;
367 let corner = &self.rot_imgs.tt_corner;
368 ImageFrame::new(
369 [edge.cw180, edge.none, edge.cw270, edge.cw90],
370 [corner.none, corner.cw270, corner.cw90, corner.cw180],
371 Color::Rgba(0.08, 0.07, 0.04, 1.0),
372 5.0,
373 )
374 },
375 self.client,
376 self.info,
377 self.imgs,
378 self.item_imgs,
379 self.pulse,
380 self.msm,
381 self.rbm,
382 Some(self.inventory),
383 self.localized_strings,
384 self.item_i18n,
385 )
386 .title_font_size(self.fonts.cyri.scale(20))
387 .parent(ui.window)
388 .desc_font_size(self.fonts.cyri.scale(12))
389 .font_id(self.fonts.cyri.conrod_id)
390 .desc_text_color(TEXT_COLOR);
391 let tabs_tooltip = Tooltip::new({
393 let edge = &self.rot_imgs.tt_side;
396 let corner = &self.rot_imgs.tt_corner;
397 ImageFrame::new(
398 [edge.cw180, edge.none, edge.cw270, edge.cw90],
399 [corner.none, corner.cw270, corner.cw90, corner.cw180],
400 Color::Rgba(0.08, 0.07, 0.04, 1.0),
401 5.0,
402 )
403 })
404 .title_font_size(self.fonts.cyri.scale(15))
405 .parent(ui.window)
406 .desc_font_size(self.fonts.cyri.scale(12))
407 .font_id(self.fonts.cyri.conrod_id)
408 .desc_text_color(TEXT_COLOR);
409
410 let crafting_pos = self.global_state.settings.hud_position.crafting;
411 let crafting_window_size = Vec2::new(470.0, 460.0);
412
413 Image::new(self.imgs.crafting_window)
415 .bottom_right_with_margins_on(ui.window, crafting_pos.y, crafting_pos.x)
416 .color(Some(UI_MAIN))
417 .w_h(crafting_window_size.x, crafting_window_size.y)
418 .set(state.ids.window, ui);
419
420 Image::new(self.imgs.crafting_frame)
422 .middle_of(state.ids.window)
423 .color(Some(UI_HIGHLIGHT_0))
424 .wh_of(state.ids.window)
425 .set(state.ids.window_frame, ui);
426
427 Image::new(self.imgs.crafting_icon_bordered)
429 .w_h(38.0, 38.0)
430 .top_left_with_margins_on(state.ids.window_frame, 4.0, 4.0)
431 .set(state.ids.icon, ui);
432
433 if Button::image(self.imgs.close_btn)
435 .w_h(24.0, 25.0)
436 .hover_image(self.imgs.close_btn_hover)
437 .press_image(self.imgs.close_btn_press)
438 .top_right_with_margins_on(state.ids.window, 0.0, 0.0)
439 .set(state.ids.close, ui)
440 .was_clicked()
441 {
442 events.push(Event::Close);
443 }
444
445 Text::new(&self.localized_strings.get_msg("hud-crafting"))
447 .mid_top_with_margin_on(state.ids.window_frame, 9.0)
448 .font_id(self.fonts.cyri.conrod_id)
449 .font_size(self.fonts.cyri.scale(20))
450 .color(TEXT_COLOR)
451 .set(state.ids.title_main, ui);
452
453 Rectangle::fill_with([184.0, 380.0], color::TRANSPARENT)
455 .top_left_with_margins_on(state.ids.window_frame, 72.0, 5.0)
456 .scroll_kids_vertically()
457 .set(state.ids.align_rec, ui);
458 Rectangle::fill_with([274.0, 340.0], color::TRANSPARENT)
459 .top_right_with_margins_on(state.ids.window, 72.0, 5.0)
460 .scroll_kids_vertically()
461 .set(state.ids.align_ing, ui);
462
463 if state.ids.category_bgs.len() < CraftingTab::iter().enumerate().len() {
465 state.update(|s| {
466 s.ids.category_bgs.resize(
467 CraftingTab::iter().enumerate().len(),
468 &mut ui.widget_id_generator(),
469 )
470 })
471 };
472 if state.ids.category_tabs.len() < CraftingTab::iter().enumerate().len() {
473 state.update(|s| {
474 s.ids.category_tabs.resize(
475 CraftingTab::iter().enumerate().len(),
476 &mut ui.widget_id_generator(),
477 )
478 })
479 };
480 if state.ids.category_imgs.len() < CraftingTab::iter().enumerate().len() {
481 state.update(|s| {
482 s.ids.category_imgs.resize(
483 CraftingTab::iter().enumerate().len(),
484 &mut ui.widget_id_generator(),
485 )
486 })
487 };
488 let sel_crafting_tab = &self.show.crafting_fields.crafting_tab;
489 for (i, crafting_tab) in CraftingTab::iter()
490 .filter(|tab| !tab.is_adhoc())
491 .enumerate()
492 {
493 let tab_img = crafting_tab.img_id(self.imgs);
494 let mut bg = Image::new(self.imgs.pixel)
496 .w_h(40.0, 30.0)
497 .color(Some(UI_MAIN));
498 if i == 0 {
499 bg = bg.top_left_with_margins_on(state.ids.window_frame, 50.0, -40.0)
500 } else {
501 bg = bg.down_from(state.ids.category_bgs[i - 1], 0.0)
502 };
503 bg.set(state.ids.category_bgs[i], ui);
504 if Button::image(if crafting_tab == *sel_crafting_tab {
506 self.imgs.wpn_icon_border_pressed
507 } else {
508 self.imgs.wpn_icon_border
509 })
510 .wh_of(state.ids.category_bgs[i])
511 .middle_of(state.ids.category_bgs[i])
512 .hover_image(if crafting_tab == *sel_crafting_tab {
513 self.imgs.wpn_icon_border_pressed
514 } else {
515 self.imgs.wpn_icon_border_mo
516 })
517 .press_image(if crafting_tab == *sel_crafting_tab {
518 self.imgs.wpn_icon_border_pressed
519 } else {
520 self.imgs.wpn_icon_border_press
521 })
522 .with_tooltip(
523 self.tooltip_manager,
524 &self.localized_strings.get_msg(crafting_tab.name_key()),
525 "",
526 &tabs_tooltip,
527 TEXT_COLOR,
528 )
529 .set(state.ids.category_tabs[i], ui)
530 .was_clicked()
531 {
532 events.push(Event::ChangeCraftingTab(crafting_tab))
533 };
534 Image::new(tab_img)
536 .middle_of(state.ids.category_tabs[i])
537 .w_h(20.0, 20.0)
538 .graphics_for(state.ids.category_tabs[i])
539 .set(state.ids.category_imgs[i], ui);
540 }
541
542 let mut _lower_case_search = String::new();
545 let (search_filter, search_keys) = {
546 if let Some(key) = &self.show.crafting_fields.crafting_search_key {
547 _lower_case_search = key.as_str().to_lowercase();
548 _lower_case_search
549 .split_once(':')
550 .map(|(filter, key)| {
551 (
552 SearchFilter::parse_from_str(filter),
553 key.split_whitespace().collect(),
554 )
555 })
556 .unwrap_or((
557 SearchFilter::None,
558 _lower_case_search.split_whitespace().collect(),
559 ))
560 } else {
561 (SearchFilter::None, vec![])
562 }
563 };
564
565 let make_pseudo_recipe = |craft_sprite| Recipe {
566 output: (
567 Arc::<ItemDef>::load_expect_cloned("common.items.weapons.empty.empty"),
568 0,
569 ),
570 inputs: Vec::new(),
571 craft_sprite: Some(craft_sprite),
572 };
573
574 let weapon_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
575 let metal_comp_recipe = make_pseudo_recipe(SpriteKind::Anvil);
576 let wood_comp_recipe = make_pseudo_recipe(SpriteKind::CraftingBench);
577 let repair_recipe = make_pseudo_recipe(SpriteKind::RepairBench);
578
579 let pseudo_entries = {
583 let mut pseudo_entries = BTreeMap::new();
584 pseudo_entries.insert(
585 String::from("veloren.core.pseudo_recipe.modular_weapon"),
586 (
587 &weapon_recipe,
588 self.localized_strings
589 .get_msg("pseudo-recipe-modular_weapon-modular_weapon")
590 .to_string(),
591 CraftingTab::Weapon,
592 ),
593 );
594 pseudo_entries.insert(
595 String::from("veloren.core.pseudo_recipe.modular_weapon_component.sword"),
596 (
597 &metal_comp_recipe,
598 self.localized_strings
599 .get_msg("pseudo-recipe-modular_weapon-sword")
600 .to_string(),
601 CraftingTab::Weapon,
602 ),
603 );
604 pseudo_entries.insert(
605 String::from("veloren.core.pseudo_recipe.modular_weapon_component.axe"),
606 (
607 &metal_comp_recipe,
608 self.localized_strings
609 .get_msg("pseudo-recipe-modular_weapon-axe")
610 .to_string(),
611 CraftingTab::Weapon,
612 ),
613 );
614 pseudo_entries.insert(
615 String::from("veloren.core.pseudo_recipe.modular_weapon_component.hammer"),
616 (
617 &metal_comp_recipe,
618 self.localized_strings
619 .get_msg("pseudo-recipe-modular_weapon-hammer")
620 .to_string(),
621 CraftingTab::Weapon,
622 ),
623 );
624 pseudo_entries.insert(
625 String::from("veloren.core.pseudo_recipe.modular_weapon_component.bow"),
626 (
627 &wood_comp_recipe,
628 self.localized_strings
629 .get_msg("pseudo-recipe-modular_weapon-bow")
630 .to_string(),
631 CraftingTab::Weapon,
632 ),
633 );
634 pseudo_entries.insert(
635 String::from("veloren.core.pseudo_recipe.modular_weapon_component.staff"),
636 (
637 &wood_comp_recipe,
638 self.localized_strings
639 .get_msg("pseudo-recipe-modular_weapon-staff")
640 .to_string(),
641 CraftingTab::Weapon,
642 ),
643 );
644 pseudo_entries.insert(
645 String::from("veloren.core.pseudo_recipe.modular_weapon_component.sceptre"),
646 (
647 &wood_comp_recipe,
648 self.localized_strings
649 .get_msg("pseudo-recipe-modular_weapon-sceptre")
650 .to_string(),
651 CraftingTab::Weapon,
652 ),
653 );
654 pseudo_entries.insert(
655 String::from("veloren.core.pseudo_recipe.repair"),
656 (
657 &repair_recipe,
658 self.localized_strings
659 .get_msg("pseudo-recipe-repair")
660 .to_string(),
661 CraftingTab::All,
662 ),
663 );
664 pseudo_entries
665 };
666
667 let search = |item: &Arc<ItemDef>| {
672 let (name_key, _) = item.i18n(self.item_i18n);
673 let fallback_name = self
674 .localized_strings
675 .get_content_fallback(&name_key)
676 .to_lowercase();
677 let name = self.localized_strings.get_content(&name_key).to_lowercase();
678
679 search_keys.iter().all(|&substring| {
680 name.contains(substring)
681 || fallback_name.contains(substring)
682 || name_key.hacky_descriptor().contains(substring)
683 })
684 };
685 let known_recipes = self
686 .inventory
687 .available_recipes_iter(self.rbm)
688 .map(|r| r.0.as_str())
689 .collect::<HashSet<_>>();
690 let recipe_source = if self.settings.gameplay.show_all_recipes {
691 Either::Left(
692 self.rbm
693 .iter()
694 .map(|r| (r, known_recipes.contains(r.0.as_str()))),
695 )
696 } else {
697 Either::Right(
698 self.inventory
699 .available_recipes_iter(self.rbm)
700 .map(|r| (r, true)),
701 )
702 };
703 let mut ordered_recipes: Vec<_> = recipe_source
704 .filter(|((_, recipe), _)| match search_filter {
705 SearchFilter::None => {
706 search(&recipe.output.0)
707 },
708 SearchFilter::Input => recipe.inputs().any(|(input, _, _)| {
709 let search_tag = |name: &str| {
710 search_keys
711 .iter()
712 .all(|&substring| name.contains(substring))
713 };
714 match input {
715 RecipeInput::Item(def) => search(def),
716 RecipeInput::Tag(tag) => search_tag(tag.name()),
717 RecipeInput::TagSameItem(tag) => search_tag(tag.name()),
718 RecipeInput::ListSameItem(defs) => {
719 defs.iter().any(search)
720 },
721 }
722 }),
723 SearchFilter::Nonexistent => false,
724 })
725 .map(|((name, recipe), known)| {
726 let has_materials = self.client.available_recipes().get(name.as_str()).is_some();
727 let is_craftable =
728 self.client
729 .available_recipes()
730 .get(name.as_str()).is_some_and(|cs| {
731 cs.is_none_or(|cs| {
732 Some(cs) == self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
733 })
734 });
735 (name, recipe, is_craftable, has_materials, known)
736 })
737 .chain(
738 pseudo_entries
739 .iter()
740 .filter(|(_, (_, _, tab))| *sel_crafting_tab == CraftingTab::All || sel_crafting_tab == tab)
742 .filter(|(_, (_, output_name, _))| {
744 match search_filter {
745 SearchFilter::None => {
746 let output_name = output_name.to_lowercase();
747 search_keys
748 .iter()
749 .all(|&substring| output_name.contains(substring))
750 },
751 SearchFilter::Input => false,
754 SearchFilter::Nonexistent => false,
755 }
756 })
757 .map(|(recipe_name, (recipe, _, _))| {
758 (
759 recipe_name,
760 *recipe,
761 self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
762 == recipe.craft_sprite,
763 true,
764 true,
765 )
766 }),
767 )
768 .collect();
769
770 ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials, known)| {
771 (
772 !known,
773 !is_craftable,
774 !has_materials,
775 recipe.output.0.quality(),
776 {
777 let (title, _) = recipe.output.0.i18n(self.item_i18n);
778 self.localized_strings.get_content(&title)
779 },
780 )
781 });
782
783 let recipe_list_length = if self.settings.gameplay.show_all_recipes {
785 self.rbm.iter().count()
786 } else {
787 self.inventory.recipe_book_len()
788 } + pseudo_entries.len();
789
790 if state.ids.recipe_list_btns.len() < recipe_list_length {
791 state.update(|state| {
792 state
793 .ids
794 .recipe_list_btns
795 .resize(recipe_list_length, &mut ui.widget_id_generator())
796 });
797 }
798 if state.ids.recipe_list_labels.len() < recipe_list_length {
799 state.update(|state| {
800 state
801 .ids
802 .recipe_list_labels
803 .resize(recipe_list_length, &mut ui.widget_id_generator())
804 });
805 }
806 if state.ids.recipe_list_quality_indicators.len() < recipe_list_length {
807 state.update(|state| {
808 state
809 .ids
810 .recipe_list_quality_indicators
811 .resize(recipe_list_length, &mut ui.widget_id_generator())
812 });
813 }
814 if state.ids.recipe_list_materials_indicators.len() < recipe_list_length {
815 state.update(|state| {
816 state
817 .ids
818 .recipe_list_materials_indicators
819 .resize(recipe_list_length, &mut ui.widget_id_generator())
820 });
821 }
822 for (i, (name, recipe, is_craftable, has_materials, knows_recipe)) in ordered_recipes
823 .into_iter()
824 .filter(|(_, recipe, _, _, _)| self.show.crafting_fields.crafting_tab.satisfies(recipe))
825 .enumerate()
826 {
827 let button = Button::image(if state.selected_recipe.as_ref() == Some(name) {
828 self.imgs.selection
829 } else {
830 self.imgs.nothing
831 })
832 .and(|button| {
833 if i == 0 {
834 button.top_left_with_margins_on(state.ids.align_rec, 2.0, 7.0)
835 } else {
836 button.down_from(state.ids.recipe_list_btns[i - 1], 5.0)
837 }
838 })
839 .w(171.0)
840 .hover_image(self.imgs.selection_hover)
841 .press_image(self.imgs.selection_press)
842 .image_color(color::rgba(1.0, 0.82, 0.27, 1.0));
843
844 let title;
845 let recipe_name =
846 if let Some((_recipe, pseudo_name, _filter_tab)) = pseudo_entries.get(name) {
847 pseudo_name
848 } else {
849 (title, _) = util::item_text(
850 recipe.output.0.as_ref(),
851 self.localized_strings,
852 self.item_i18n,
853 );
854 &title
855 };
856
857 let text = Text::new(recipe_name)
858 .color(if is_craftable {
859 TEXT_COLOR
860 } else {
861 TEXT_GRAY_COLOR
862 })
863 .font_size(self.fonts.cyri.scale(12))
864 .font_id(self.fonts.cyri.conrod_id)
865 .w(163.0)
866 .mid_top_with_margin_on(state.ids.recipe_list_btns[i], 3.0)
867 .graphics_for(state.ids.recipe_list_btns[i])
868 .center_justify();
869
870 let text_height = match text.get_y_dimension(ui) {
871 Dimension::Absolute(y) => y,
872 _ => 0.0,
873 };
874 let button_height = (text_height + 7.0).max(20.0);
875
876 if button
877 .h(button_height)
878 .set(state.ids.recipe_list_btns[i], ui)
879 .was_clicked()
880 {
881 if state.selected_recipe.as_ref() == Some(name) {
882 state.update(|s| s.selected_recipe = None);
883 } else {
884 if self.show.crafting_fields.crafting_tab.is_adhoc() {
885 events.push(Event::ChangeCraftingTab(CraftingTab::All));
888 }
889 state.update(|s| s.selected_recipe = Some(name.clone()));
890 }
891 events.push(Event::ClearRecipeInputs);
892 }
893 text.set(state.ids.recipe_list_labels[i], ui);
895
896 let color::Hsla(h, s, l, _) = get_quality_col(recipe.output.0.quality()).to_hsl();
898 let val_multiplier = if is_craftable { 0.7 } else { 0.5 };
899 let quality_col = color::hsl(
904 h,
905 s * val_multiplier * f32::min(l, 1.0 - l)
906 / f32::min(l * val_multiplier, 1.0 - l * val_multiplier),
907 l * val_multiplier,
908 );
909
910 Button::image(self.imgs.quality_indicator)
911 .image_color(quality_col)
912 .w_h(4.0, button_height)
913 .left_from(state.ids.recipe_list_btns[i], 1.0)
914 .graphics_for(state.ids.recipe_list_btns[i])
915 .set(state.ids.recipe_list_quality_indicators[i], ui);
916
917 if !knows_recipe {
919 let recipe_img = "Recipe";
920
921 Button::image(animate_by_pulse(
922 &self
923 .item_imgs
924 .img_ids_or_not_found_img(ItemKey::Simple(recipe_img.to_string())),
925 self.pulse,
926 ))
927 .image_color(color::LIGHT_RED)
928 .w_h(button_height - 8.0, button_height - 8.0)
929 .top_left_with_margins_on(state.ids.recipe_list_btns[i], 4.0, 4.0)
930 .graphics_for(state.ids.recipe_list_btns[i])
931 .set(state.ids.recipe_list_materials_indicators[i], ui);
932 } else if has_materials && !is_craftable {
933 let station_img = match recipe.craft_sprite {
934 Some(SpriteKind::Anvil) => Some("Anvil"),
935 Some(SpriteKind::Cauldron) => Some("Cauldron"),
936 Some(SpriteKind::CookingPot) => Some("CookingPot"),
937 Some(SpriteKind::CraftingBench) => Some("CraftingBench"),
938 Some(SpriteKind::Forge) => Some("Forge"),
939 Some(SpriteKind::Loom) => Some("Loom"),
940 Some(SpriteKind::SpinningWheel) => Some("SpinningWheel"),
941 Some(SpriteKind::TanningRack) => Some("TanningRack"),
942 Some(SpriteKind::DismantlingBench) => Some("DismantlingBench"),
943 Some(SpriteKind::RepairBench) => Some("RepairBench"),
944 _ => None,
945 };
946
947 if let Some(station_img_str) = station_img {
948 Button::image(animate_by_pulse(
949 &self
950 .item_imgs
951 .img_ids_or_not_found_img(ItemKey::Simple(station_img_str.to_string())),
952 self.pulse,
953 ))
954 .image_color(color::LIGHT_RED)
955 .w_h(button_height - 8.0, button_height - 8.0)
956 .top_left_with_margins_on(state.ids.recipe_list_btns[i], 4.0, 4.0)
957 .graphics_for(state.ids.recipe_list_btns[i])
958 .set(state.ids.recipe_list_materials_indicators[i], ui);
959 }
960 }
961 }
962
963 if self.show.crafting_fields.crafting_tab.is_adhoc() {
966 state.update(|s| s.selected_recipe = None);
967 }
968
969 if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() {
971 Some(selected_recipe) => {
972 if let Some((modular_recipe, _pseudo_name, _filter_tab)) =
973 pseudo_entries.get(selected_recipe)
974 {
975 Some((selected_recipe, *modular_recipe))
976 } else {
977 self.inventory
978 .get_recipe(selected_recipe, self.rbm)
979 .map(|r| (selected_recipe, r))
980 }
981 },
982 None => None,
983 } {
984 let recipe_name = String::from(recipe_name);
985
986 let title;
987 let title = if let Some((_recipe, pseudo_name, _filter_tab)) =
988 pseudo_entries.get(&recipe_name)
989 {
990 pseudo_name
991 } else {
992 (title, _) = util::item_text(
993 recipe.output.0.as_ref(),
994 self.localized_strings,
995 self.item_i18n,
996 );
997 &title
998 };
999
1000 Text::new(title)
1002 .mid_top_with_margin_on(state.ids.align_ing, -22.0)
1003 .font_id(self.fonts.cyri.conrod_id)
1004 .font_size(self.fonts.cyri.scale(14))
1005 .color(TEXT_COLOR)
1006 .parent(state.ids.window)
1007 .set(state.ids.title_ing, ui);
1008
1009 #[derive(Clone, Copy, Debug)]
1010 enum RecipeKind {
1011 ModularWeapon,
1012 Component(ToolKind),
1013 Simple,
1014 Repair,
1015 }
1016
1017 let recipe_kind = match recipe_name.as_str() {
1018 "veloren.core.pseudo_recipe.modular_weapon" => RecipeKind::ModularWeapon,
1019 "veloren.core.pseudo_recipe.modular_weapon_component.sword" => {
1020 RecipeKind::Component(ToolKind::Sword)
1021 },
1022 "veloren.core.pseudo_recipe.modular_weapon_component.axe" => {
1023 RecipeKind::Component(ToolKind::Axe)
1024 },
1025 "veloren.core.pseudo_recipe.modular_weapon_component.hammer" => {
1026 RecipeKind::Component(ToolKind::Hammer)
1027 },
1028 "veloren.core.pseudo_recipe.modular_weapon_component.bow" => {
1029 RecipeKind::Component(ToolKind::Bow)
1030 },
1031 "veloren.core.pseudo_recipe.modular_weapon_component.staff" => {
1032 RecipeKind::Component(ToolKind::Staff)
1033 },
1034 "veloren.core.pseudo_recipe.modular_weapon_component.sceptre" => {
1035 RecipeKind::Component(ToolKind::Sceptre)
1036 },
1037 "veloren.core.pseudo_recipe.repair" => RecipeKind::Repair,
1038 _ => RecipeKind::Simple,
1039 };
1040
1041 let mut slot_maker = SlotMaker {
1042 empty_slot: self.imgs.inv_slot,
1043 filled_slot: self.imgs.inv_slot,
1044 selected_slot: self.imgs.inv_slot_sel,
1045 background_color: Some(UI_MAIN),
1046 content_size: ContentSize {
1047 width_height_ratio: 1.0,
1048 max_fraction: 0.75,
1049 },
1050 selected_content_scale: 1.067,
1051 amount_font: self.fonts.cyri.conrod_id,
1052 amount_margins: Vec2::new(-4.0, 0.0),
1053 amount_font_size: self.fonts.cyri.scale(12),
1054 amount_text_color: TEXT_COLOR,
1055 content_source: self.inventory,
1056 image_source: self.item_imgs,
1057 slot_manager: Some(self.slot_manager),
1058 pulse: self.pulse,
1059 };
1060
1061 let (craft_slot_1, craft_slot_2, can_perform, recipe_known) = match recipe_kind {
1063 RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
1064 if state.ids.craft_slots.len() < 2 {
1065 state.update(|s| {
1066 s.ids.craft_slots.resize(2, &mut ui.widget_id_generator());
1067 });
1068 }
1069
1070 Text::new(&self.localized_strings.get_msg("hud-crafting-modular_desc"))
1072 .mid_top_of(state.ids.align_ing)
1073 .w(264.0)
1074 .center_justify()
1075 .font_id(self.fonts.cyri.conrod_id)
1076 .font_size(self.fonts.cyri.scale(13))
1077 .color(TEXT_COLOR)
1078 .set(state.ids.modular_desc_txt, ui);
1079
1080 Image::new(self.imgs.crafting_modular_art)
1082 .down_from(state.ids.modular_desc_txt, 15.0)
1083 .align_middle_x()
1084 .w_h(168.0, 250.0)
1085 .set(state.ids.modular_art, ui);
1086
1087 let primary_slot = CraftSlot {
1088 index: 0,
1089 slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
1090 requirement: match recipe_kind {
1091 RecipeKind::ModularWeapon => |item, _, _| {
1092 matches!(
1093 &*item.kind(),
1094 ItemKind::ModularComponent(
1095 ModularComponent::ToolPrimaryComponent { .. }
1096 )
1097 )
1098 },
1099 RecipeKind::Component(_) => |item, comp_recipes, info| {
1100 if let Some(CraftSlotInfo::Tool(toolkind)) = info {
1101 comp_recipes
1102 .iter()
1103 .filter(|(key, _)| key.toolkind == toolkind)
1104 .any(|(key, _)| {
1105 Some(key.material.as_str())
1106 == item.item_definition_id().itemdef_id()
1107 })
1108 } else {
1109 false
1110 }
1111 },
1112 RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
1113 },
1114 info: match recipe_kind {
1115 RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
1116 RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
1117 None
1118 },
1119 },
1120 };
1121
1122 let primary_slot_widget = slot_maker
1123 .fabricate(primary_slot, [40.0; 2])
1124 .top_left_with_margins_on(state.ids.modular_art, 4.0, 4.0)
1125 .parent(state.ids.align_ing);
1126
1127 if let Some(item) = primary_slot.item(self.inventory) {
1128 primary_slot_widget
1129 .with_item_tooltip(
1130 self.item_tooltip_manager,
1131 core::iter::once(item as &dyn ItemDesc),
1132 &None,
1133 &item_tooltip,
1134 )
1135 .set(state.ids.craft_slots[0], ui);
1136 } else {
1137 let (tooltip_title, tooltip_desc) = match recipe_kind {
1138 RecipeKind::ModularWeapon => (
1139 self.localized_strings
1140 .get_msg("hud-crafting-mod_weap_prim_slot_title"),
1141 self.localized_strings
1142 .get_msg("hud-crafting-mod_weap_prim_slot_desc"),
1143 ),
1144 RecipeKind::Component(
1145 ToolKind::Sword | ToolKind::Axe | ToolKind::Hammer,
1146 ) => (
1147 self.localized_strings
1148 .get_msg("hud-crafting-mod_comp_metal_prim_slot_title"),
1149 self.localized_strings
1150 .get_msg("hud-crafting-mod_comp_metal_prim_slot_desc"),
1151 ),
1152 RecipeKind::Component(
1153 ToolKind::Bow | ToolKind::Staff | ToolKind::Sceptre,
1154 ) => (
1155 self.localized_strings
1156 .get_msg("hud-crafting-mod_comp_wood_prim_slot_title"),
1157 self.localized_strings
1158 .get_msg("hud-crafting-mod_comp_wood_prim_slot_desc"),
1159 ),
1160 RecipeKind::Component(_) | RecipeKind::Simple | RecipeKind::Repair => {
1161 (Cow::Borrowed(""), Cow::Borrowed(""))
1162 },
1163 };
1164 primary_slot_widget
1165 .with_tooltip(
1166 self.tooltip_manager,
1167 &tooltip_title,
1168 &tooltip_desc,
1169 &tabs_tooltip,
1170 TEXT_COLOR,
1171 )
1172 .set(state.ids.craft_slots[0], ui);
1173 }
1174
1175 let secondary_slot = CraftSlot {
1176 index: 1,
1177 slot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
1178 requirement: match recipe_kind {
1179 RecipeKind::ModularWeapon => |item, _, _| {
1180 matches!(
1181 &*item.kind(),
1182 ItemKind::ModularComponent(
1183 ModularComponent::ToolSecondaryComponent { .. }
1184 )
1185 )
1186 },
1187 RecipeKind::Component(_) => |item, comp_recipes, info| {
1188 if let Some(CraftSlotInfo::Tool(toolkind)) = info {
1189 comp_recipes
1190 .iter()
1191 .filter(|(key, _)| key.toolkind == toolkind)
1192 .any(|(key, _)| {
1193 key.modifier.as_deref()
1194 == item.item_definition_id().itemdef_id()
1195 })
1196 } else {
1197 false
1198 }
1199 },
1200 RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
1201 },
1202 info: match recipe_kind {
1203 RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
1204 RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
1205 None
1206 },
1207 },
1208 };
1209
1210 let secondary_slot_widget = slot_maker
1211 .fabricate(secondary_slot, [40.0; 2])
1212 .top_right_with_margins_on(state.ids.modular_art, 4.0, 4.0)
1213 .parent(state.ids.align_ing);
1214
1215 if let Some(item) = secondary_slot.item(self.inventory) {
1216 secondary_slot_widget
1217 .with_item_tooltip(
1218 self.item_tooltip_manager,
1219 core::iter::once(item as &dyn ItemDesc),
1220 &None,
1221 &item_tooltip,
1222 )
1223 .set(state.ids.craft_slots[1], ui);
1224 } else {
1225 let (tooltip_title, tooltip_desc) = match recipe_kind {
1226 RecipeKind::ModularWeapon => (
1227 self.localized_strings
1228 .get_msg("hud-crafting-mod_weap_sec_slot_title"),
1229 self.localized_strings
1230 .get_msg("hud-crafting-mod_weap_sec_slot_desc"),
1231 ),
1232 RecipeKind::Component(_) => (
1233 self.localized_strings
1234 .get_msg("hud-crafting-mod_comp_sec_slot_title"),
1235 self.localized_strings
1236 .get_msg("hud-crafting-mod_comp_sec_slot_desc"),
1237 ),
1238 RecipeKind::Simple | RecipeKind::Repair => {
1239 (Cow::Borrowed(""), Cow::Borrowed(""))
1240 },
1241 };
1242 secondary_slot_widget
1243 .with_tooltip(
1244 self.tooltip_manager,
1245 &tooltip_title,
1246 &tooltip_desc,
1247 &tabs_tooltip,
1248 TEXT_COLOR,
1249 )
1250 .set(state.ids.craft_slots[1], ui);
1251 }
1252
1253 let prim_item_placed = primary_slot.slot.is_some();
1254 let sec_item_placed = secondary_slot.slot.is_some();
1255
1256 let prim_icon = match recipe_kind {
1257 RecipeKind::ModularWeapon => self.imgs.icon_primary_comp,
1258 RecipeKind::Component(ToolKind::Sword) => self.imgs.icon_ingot,
1259 RecipeKind::Component(ToolKind::Axe) => self.imgs.icon_ingot,
1260 RecipeKind::Component(ToolKind::Hammer) => self.imgs.icon_ingot,
1261 RecipeKind::Component(ToolKind::Bow) => self.imgs.icon_log,
1262 RecipeKind::Component(ToolKind::Staff) => self.imgs.icon_log,
1263 RecipeKind::Component(ToolKind::Sceptre) => self.imgs.icon_log,
1264 RecipeKind::Component(ToolKind::Shield) => self.imgs.icon_ingot,
1265 _ => self.imgs.not_found,
1266 };
1267
1268 let sec_icon = match recipe_kind {
1269 RecipeKind::ModularWeapon => self.imgs.icon_secondary_comp,
1270 RecipeKind::Component(_) => self.imgs.icon_claw,
1271 _ => self.imgs.not_found,
1272 };
1273
1274 Image::new(self.imgs.inv_slot)
1276 .w_h(80.0, 80.0)
1277 .mid_bottom_with_margin_on(state.ids.modular_art, 16.0)
1278 .parent(state.ids.align_ing)
1279 .set(state.ids.output_img_frame, ui);
1280 let bg_col = Color::Rgba(1.0, 1.0, 1.0, 0.4);
1281 if !prim_item_placed {
1282 Image::new(prim_icon)
1283 .middle_of(state.ids.craft_slots[0])
1284 .color(Some(bg_col))
1285 .w_h(34.0, 34.0)
1286 .graphics_for(state.ids.craft_slots[0])
1287 .set(state.ids.modular_wep_ing_1_bg, ui);
1288 }
1289 if !sec_item_placed {
1290 Image::new(sec_icon)
1291 .middle_of(state.ids.craft_slots[1])
1292 .color(Some(bg_col))
1293 .w_h(50.0, 50.0)
1294 .graphics_for(state.ids.craft_slots[1])
1295 .set(state.ids.modular_wep_ing_2_bg, ui);
1296 }
1297
1298 let ability_map = &AbilityMap::load().read();
1299 let msm = &MaterialStatManifest::load().read();
1300
1301 let (output_item, recipe_known) = match recipe_kind {
1302 RecipeKind::ModularWeapon => {
1303 let item = if let Some((primary_comp, toolkind, hand_restriction)) =
1304 primary_slot.item(self.inventory).and_then(|item| {
1305 if let ItemKind::ModularComponent(
1306 ModularComponent::ToolPrimaryComponent {
1307 toolkind,
1308 hand_restriction,
1309 ..
1310 },
1311 ) = &*item.kind()
1312 {
1313 Some((item, *toolkind, *hand_restriction))
1314 } else {
1315 None
1316 }
1317 }) {
1318 secondary_slot
1319 .item(self.inventory)
1320 .filter(|item| {
1321 matches!(
1322 &*item.kind(),
1323 ItemKind::ModularComponent(
1324 ModularComponent::ToolSecondaryComponent { toolkind: toolkind_b, hand_restriction: hand_restriction_b, .. }
1325 ) if toolkind == *toolkind_b && modular::compatible_handedness(hand_restriction, *hand_restriction_b)
1326 )
1327 })
1328 .map(|secondary_comp| {
1329 Item::new_from_item_base(
1330 ItemBase::Modular(modular::ModularBase::Tool),
1331 vec![
1332 primary_comp.duplicate(ability_map, msm),
1333 secondary_comp.duplicate(ability_map, msm),
1334 ],
1335 ability_map,
1336 msm,
1337 )
1338 })
1339 } else {
1340 None
1341 };
1342 (item, true)
1343 },
1344 RecipeKind::Component(toolkind) => {
1345 if let Some(material) =
1346 primary_slot.item(self.inventory).and_then(|item| {
1347 item.item_definition_id().itemdef_id().map(String::from)
1348 })
1349 {
1350 let component_key = ComponentKey {
1351 toolkind,
1352 material,
1353 modifier: secondary_slot.item(self.inventory).and_then(
1354 |item| {
1355 item.item_definition_id().itemdef_id().map(String::from)
1356 },
1357 ),
1358 };
1359 self.client
1360 .component_recipe_book()
1361 .get(&component_key)
1362 .map(|component_recipe| {
1363 let item = component_recipe.item_output(ability_map, msm);
1364 let learned = self
1365 .inventory
1366 .recipe_is_known(&component_recipe.recipe_book_key);
1367 (item, learned)
1368 })
1369 .map_or((None, true), |(item, known)| (Some(item), known))
1370 } else {
1371 (None, true)
1372 }
1373 },
1374 RecipeKind::Simple | RecipeKind::Repair => (None, true),
1375 };
1376
1377 if let Some(output_item) = output_item {
1378 let (name, _) =
1379 util::item_text(&output_item, self.localized_strings, self.item_i18n);
1380 Button::image(animate_by_pulse(
1381 &self
1382 .item_imgs
1383 .img_ids_or_not_found_img(ItemKey::from(&output_item)),
1384 self.pulse,
1385 ))
1386 .w_h(55.0, 55.0)
1387 .label(&name)
1388 .label_color(TEXT_COLOR)
1389 .label_font_size(self.fonts.cyri.scale(14))
1390 .label_font_id(self.fonts.cyri.conrod_id)
1391 .label_y(conrod_core::position::Relative::Scalar(-64.0))
1392 .label_x(conrod_core::position::Relative::Scalar(0.0))
1393 .middle_of(state.ids.output_img_frame)
1394 .with_item_tooltip(
1395 self.item_tooltip_manager,
1396 core::iter::once(&output_item as &dyn ItemDesc),
1397 &None,
1398 &item_tooltip,
1399 )
1400 .set(state.ids.output_img, ui);
1401 (
1402 primary_slot.slot,
1403 secondary_slot.slot,
1404 self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1405 == recipe.craft_sprite,
1406 recipe_known,
1407 )
1408 } else {
1409 Image::new(self.imgs.icon_mod_weap)
1410 .middle_of(state.ids.output_img_frame)
1411 .color(Some(bg_col))
1412 .w_h(70.0, 70.0)
1413 .graphics_for(state.ids.output_img)
1414 .set(state.ids.modular_wep_empty_bg, ui);
1415 (primary_slot.slot, secondary_slot.slot, false, recipe_known)
1416 }
1417 },
1418 RecipeKind::Simple => {
1419 let quality_col_img = match recipe.output.0.quality() {
1421 Quality::Low => self.imgs.inv_slot_grey,
1422 Quality::Common => self.imgs.inv_slot,
1423 Quality::Moderate => self.imgs.inv_slot_green,
1424 Quality::High => self.imgs.inv_slot_blue,
1425 Quality::Epic => self.imgs.inv_slot_purple,
1426 Quality::Legendary => self.imgs.inv_slot_gold,
1427 Quality::Artifact => self.imgs.inv_slot_orange,
1428 _ => self.imgs.inv_slot_red,
1429 };
1430
1431 Image::new(quality_col_img)
1432 .w_h(60.0, 60.0)
1433 .top_right_with_margins_on(state.ids.align_ing, 15.0, 10.0)
1434 .parent(state.ids.align_ing)
1435 .set(state.ids.output_img_frame, ui);
1436
1437 let output_text = format!("x{}", recipe.output.1);
1438 Button::image(animate_by_pulse(
1440 &self
1441 .item_imgs
1442 .img_ids_or_not_found_img((&*recipe.output.0).into()),
1443 self.pulse,
1444 ))
1445 .w_h(55.0, 55.0)
1446 .label(&output_text)
1447 .label_color(TEXT_COLOR)
1448 .label_font_size(self.fonts.cyri.scale(14))
1449 .label_font_id(self.fonts.cyri.conrod_id)
1450 .label_y(conrod_core::position::Relative::Scalar(-24.0))
1451 .label_x(conrod_core::position::Relative::Scalar(24.0))
1452 .middle_of(state.ids.output_img_frame)
1453 .with_item_tooltip(
1454 self.item_tooltip_manager,
1455 core::iter::once(&*recipe.output.0 as &dyn ItemDesc),
1456 &None,
1457 &item_tooltip,
1458 )
1459 .set(state.ids.output_img, ui);
1460
1461 if state.ids.tags_ing.len() < CraftingTab::iter().len() {
1463 state.update(|state| {
1464 state
1465 .ids
1466 .tags_ing
1467 .resize(CraftingTab::iter().len(), &mut ui.widget_id_generator())
1468 });
1469 }
1470 for (row, chunk) in CraftingTab::iter()
1471 .filter(|crafting_tab| match crafting_tab {
1472 CraftingTab::All => false,
1473 _ => crafting_tab.satisfies(recipe),
1474 })
1475 .filter(|crafting_tab| {
1476 crafting_tab != &self.show.crafting_fields.crafting_tab
1477 })
1478 .collect::<Vec<_>>()
1479 .chunks(3)
1480 .enumerate()
1481 {
1482 for (col, crafting_tab) in chunk.iter().rev().enumerate() {
1483 let i = 3 * row + col;
1484 let icon = Image::new(crafting_tab.img_id(self.imgs))
1485 .w_h(20.0, 20.0)
1486 .parent(state.ids.window);
1487 let icon = if col == 0 {
1488 icon.bottom_right_with_margins_on(
1489 state.ids.output_img_frame,
1490 -24.0 - 24.0 * (row as f64),
1491 4.0,
1492 )
1493 } else {
1494 icon.left_from(state.ids.tags_ing[i - 1], 4.0)
1495 };
1496 icon.with_tooltip(
1497 self.tooltip_manager,
1498 &self.localized_strings.get_msg(crafting_tab.name_key()),
1499 "",
1500 &tabs_tooltip,
1501 TEXT_COLOR,
1502 )
1503 .set(state.ids.tags_ing[i], ui);
1504 }
1505 }
1506 (
1507 None,
1508 None,
1509 self.client
1510 .available_recipes()
1511 .get(&recipe_name)
1512 .is_some_and(|cs| {
1513 cs.is_none_or(|cs| {
1514 Some(cs)
1515 == self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1516 })
1517 }),
1518 true,
1519 )
1520 },
1521 RecipeKind::Repair => {
1522 if state.ids.craft_slots.is_empty() {
1523 state.update(|s| {
1524 s.ids.craft_slots.resize(1, &mut ui.widget_id_generator());
1525 });
1526 }
1527 if state.ids.repair_buttons.len() < 2 {
1528 state.update(|s| {
1529 s.ids
1530 .repair_buttons
1531 .resize(2, &mut ui.widget_id_generator());
1532 });
1533 }
1534
1535 Text::new(&self.localized_strings.get_msg("hud-crafting-repair_desc"))
1537 .mid_top_of(state.ids.align_ing)
1538 .w(264.0)
1539 .center_justify()
1540 .font_id(self.fonts.cyri.conrod_id)
1541 .font_size(self.fonts.cyri.scale(13))
1542 .color(TEXT_COLOR)
1543 .set(state.ids.modular_desc_txt, ui);
1544
1545 let repair_slot = CraftSlot {
1547 index: 0,
1548 slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
1549 requirement: |item, _, _| item.durability_lost().is_some_and(|d| d > 0),
1550 info: None,
1551 };
1552
1553 let repair_slot_widget = slot_maker
1554 .fabricate(repair_slot, [80.0; 2])
1555 .down_from(state.ids.modular_desc_txt, 15.0)
1556 .align_middle_x()
1557 .parent(state.ids.align_ing);
1558
1559 if let Some(item) = repair_slot.item(self.inventory) {
1560 repair_slot_widget
1561 .with_item_tooltip(
1562 self.item_tooltip_manager,
1563 core::iter::once(item as &dyn ItemDesc),
1564 &None,
1565 &item_tooltip,
1566 )
1567 .set(state.ids.craft_slots[0], ui);
1568 } else {
1569 repair_slot_widget
1570 .with_tooltip(
1571 self.tooltip_manager,
1572 &self
1573 .localized_strings
1574 .get_msg("hud-crafting-repair_slot_title"),
1575 &self
1576 .localized_strings
1577 .get_msg("hud-crafting-repair_slot_desc"),
1578 &tabs_tooltip,
1579 TEXT_COLOR,
1580 )
1581 .set(state.ids.craft_slots[0], ui);
1582 }
1583
1584 if repair_slot.slot.is_none() {
1585 Image::new(self.imgs.icon_mod_weap)
1586 .middle_of(state.ids.craft_slots[0])
1587 .w_h(70.0, 70.0)
1588 .graphics_for(state.ids.craft_slots[0])
1589 .set(state.ids.modular_wep_ing_1_bg, ui);
1590 }
1591
1592 let can_repair = |item: &Item| {
1593 item.durability_lost().is_some_and(|d| d > 0)
1596 };
1597
1598 let can_perform = self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1599 == recipe.craft_sprite;
1600
1601 let color = if can_perform {
1602 TEXT_COLOR
1603 } else {
1604 TEXT_GRAY_COLOR
1605 };
1606
1607 let btn_image_hover = if can_perform {
1608 self.imgs.button_hover
1609 } else {
1610 self.imgs.button
1611 };
1612
1613 let btn_image_press = if can_perform {
1614 self.imgs.button_press
1615 } else {
1616 self.imgs.button
1617 };
1618
1619 if Button::image(self.imgs.button)
1621 .w_h(105.0, 25.0)
1622 .hover_image(btn_image_hover)
1623 .press_image(btn_image_press)
1624 .label(
1625 &self
1626 .localized_strings
1627 .get_msg("hud-crafting-repair_equipped"),
1628 )
1629 .label_y(conrod_core::position::Relative::Scalar(1.0))
1630 .label_color(color)
1631 .label_font_size(self.fonts.cyri.scale(12))
1632 .label_font_id(self.fonts.cyri.conrod_id)
1633 .image_color(color)
1634 .down_from(state.ids.craft_slots[0], 45.0)
1635 .x_relative_to(state.ids.craft_slots[0], 0.0)
1636 .set(state.ids.repair_buttons[0], ui)
1637 .was_clicked()
1638 && can_perform
1639 {
1640 self.inventory
1641 .equipped_items_with_slot()
1642 .filter(|(_, item)| can_repair(item))
1643 .for_each(|(slot, _)| {
1644 events.push(Event::RepairItem {
1645 slot: Slot::Equip(slot),
1646 });
1647 })
1648 }
1649
1650 if Button::image(self.imgs.button)
1652 .w_h(105.0, 25.0)
1653 .hover_image(btn_image_hover)
1654 .press_image(btn_image_press)
1655 .label(&self.localized_strings.get_msg("hud-crafting-repair_all"))
1656 .label_y(conrod_core::position::Relative::Scalar(1.0))
1657 .label_color(color)
1658 .label_font_size(self.fonts.cyri.scale(12))
1659 .label_font_id(self.fonts.cyri.conrod_id)
1660 .image_color(color)
1661 .down_from(state.ids.repair_buttons[0], 5.0)
1662 .x_relative_to(state.ids.craft_slots[0], 0.0)
1663 .set(state.ids.repair_buttons[1], ui)
1664 .was_clicked()
1665 && can_perform
1666 {
1667 self.inventory
1668 .equipped_items_with_slot()
1669 .filter(|(_, item)| can_repair(item))
1670 .for_each(|(slot, _)| {
1671 events.push(Event::RepairItem {
1672 slot: Slot::Equip(slot),
1673 });
1674 });
1675 self.inventory
1676 .slots_with_id()
1677 .filter(|(_, item)| item.as_ref().is_some_and(can_repair))
1678 .for_each(|(slot, _)| {
1679 events.push(Event::RepairItem {
1680 slot: Slot::Inventory(slot),
1681 });
1682 });
1683 }
1684
1685 let can_perform =
1686 repair_slot.item(self.inventory).is_some_and(can_repair) && can_perform;
1687
1688 (repair_slot.slot, None, can_perform, true)
1689 },
1690 };
1691
1692 Line::centred([0.0, 0.0], [274.0, 0.0])
1694 .color(color::rgba(1.0, 1.0, 1.0, 0.1))
1695 .down_from(state.ids.align_ing, 0.0)
1696 .parent(state.ids.window)
1697 .set(state.ids.btn_separator, ui);
1698
1699 let label = &match recipe_kind {
1701 RecipeKind::Repair => self
1702 .localized_strings
1703 .get_msg("hud-crafting-repair-selection"),
1704 _ => self.localized_strings.get_msg("hud-crafting-craft"),
1705 };
1706 let craft_button_init = Button::image(self.imgs.button)
1707 .w_h(105.0, 25.0)
1708 .hover_image(if can_perform {
1709 self.imgs.button_hover
1710 } else {
1711 self.imgs.button
1712 })
1713 .press_image(if can_perform {
1714 self.imgs.button_press
1715 } else {
1716 self.imgs.button
1717 })
1718 .label(label)
1719 .label_y(conrod_core::position::Relative::Scalar(1.0))
1720 .label_color(if can_perform {
1721 TEXT_COLOR
1722 } else {
1723 TEXT_GRAY_COLOR
1724 })
1725 .label_font_size(self.fonts.cyri.scale(12))
1726 .label_font_id(self.fonts.cyri.conrod_id)
1727 .image_color(if can_perform {
1728 TEXT_COLOR
1729 } else {
1730 TEXT_GRAY_COLOR
1731 })
1732 .and(|b| match recipe_kind {
1733 RecipeKind::Repair => b
1734 .down_from(state.ids.craft_slots[0], 15.0)
1735 .x_relative_to(state.ids.craft_slots[0], 0.0)
1736 .parent(state.ids.align_ing),
1737 _ => b
1738 .bottom_left_with_margins_on(state.ids.align_ing, -31.0, 10.0)
1739 .parent(state.ids.window_frame),
1740 });
1741
1742 let craft_button = if !recipe_known {
1743 craft_button_init
1744 .with_tooltip(
1745 self.tooltip_manager,
1746 &self
1747 .localized_strings
1748 .get_msg("hud-crafting-recipe-uncraftable"),
1749 &self
1750 .localized_strings
1751 .get_msg("hud-crafting-recipe-unlearned"),
1752 &tabs_tooltip,
1753 TEXT_COLOR,
1754 )
1755 .set(state.ids.btn_craft, ui)
1756 } else {
1757 craft_button_init.set(state.ids.btn_craft, ui)
1758 };
1759
1760 if craft_button.was_clicked() && can_perform {
1761 match recipe_kind {
1762 RecipeKind::ModularWeapon => {
1763 if let (
1764 Some(Slot::Inventory(primary_slot)),
1765 Some(Slot::Inventory(secondary_slot)),
1766 ) = (craft_slot_1, craft_slot_2)
1767 {
1768 events.push(Event::CraftModularWeapon {
1769 primary_slot,
1770 secondary_slot,
1771 });
1772 }
1773 },
1774 RecipeKind::Component(toolkind) => {
1775 if let Some(Slot::Inventory(primary_slot)) = craft_slot_1 {
1776 events.push(Event::CraftModularWeaponComponent {
1777 toolkind,
1778 material: primary_slot,
1779 modifier: craft_slot_2.and_then(|slot| match slot {
1780 Slot::Inventory(slot) => Some(slot),
1781 Slot::Equip(_) => None,
1782 Slot::Overflow(_) => None,
1783 }),
1784 });
1785 }
1786 },
1787 RecipeKind::Simple => events.push(Event::CraftRecipe {
1788 recipe_name,
1789 amount: 1,
1790 }),
1791 RecipeKind::Repair => {
1792 if let Some(slot) = craft_slot_1 {
1793 events.push(Event::RepairItem { slot });
1794 }
1795 },
1796 }
1797 }
1798
1799 if matches!(recipe_kind, RecipeKind::Simple)
1801 && Button::image(self.imgs.button)
1802 .w_h(105.0, 25.0)
1803 .hover_image(if can_perform {
1804 self.imgs.button_hover
1805 } else {
1806 self.imgs.button
1807 })
1808 .press_image(if can_perform {
1809 self.imgs.button_press
1810 } else {
1811 self.imgs.button
1812 })
1813 .label(&self.localized_strings.get_msg("hud-crafting-craft_all"))
1814 .label_y(conrod_core::position::Relative::Scalar(1.0))
1815 .label_color(if can_perform {
1816 TEXT_COLOR
1817 } else {
1818 TEXT_GRAY_COLOR
1819 })
1820 .label_font_size(self.fonts.cyri.scale(12))
1821 .label_font_id(self.fonts.cyri.conrod_id)
1822 .image_color(if can_perform {
1823 TEXT_COLOR
1824 } else {
1825 TEXT_GRAY_COLOR
1826 })
1827 .bottom_right_with_margins_on(state.ids.align_ing, -31.0, 10.0)
1828 .parent(state.ids.window_frame)
1829 .set(state.ids.btn_craft_all, ui)
1830 .was_clicked()
1831 && can_perform
1832 {
1833 if let Some(selected_recipe) = &state.selected_recipe {
1834 let amount = recipe.max_from_ingredients(self.inventory);
1835 if amount > 0 {
1836 events.push(Event::CraftRecipe {
1837 recipe_name: selected_recipe.to_string(),
1838 amount,
1839 });
1840 }
1841 } else {
1842 error!("State shows no selected recipe when trying to craft multiple.");
1843 }
1844 };
1845
1846 if recipe.craft_sprite.is_some() {
1848 Text::new(
1849 &self
1850 .localized_strings
1851 .get_msg("hud-crafting-req_crafting_station"),
1852 )
1853 .font_id(self.fonts.cyri.conrod_id)
1854 .font_size(self.fonts.cyri.scale(18))
1855 .color(TEXT_COLOR)
1856 .and(|t| match recipe_kind {
1857 RecipeKind::Simple => {
1858 t.top_left_with_margins_on(state.ids.align_ing, 10.0, 5.0)
1859 },
1860 RecipeKind::ModularWeapon | RecipeKind::Component(_) => t
1861 .down_from(state.ids.modular_art, 25.0)
1862 .x_place_on(state.ids.align_ing, Place::Start(Some(5.0))),
1863 RecipeKind::Repair => t
1864 .down_from(state.ids.repair_buttons[1], 20.0)
1865 .x_place_on(state.ids.align_ing, Place::Start(Some(5.0))),
1866 })
1867 .set(state.ids.req_station_title, ui);
1868 let station_img = match recipe.craft_sprite {
1869 Some(SpriteKind::Anvil) => "Anvil",
1870 Some(SpriteKind::Cauldron) => "Cauldron",
1871 Some(SpriteKind::CookingPot) => "CookingPot",
1872 Some(SpriteKind::CraftingBench) => "CraftingBench",
1873 Some(SpriteKind::Forge) => "Forge",
1874 Some(SpriteKind::Loom) => "Loom",
1875 Some(SpriteKind::SpinningWheel) => "SpinningWheel",
1876 Some(SpriteKind::TanningRack) => "TanningRack",
1877 Some(SpriteKind::DismantlingBench) => "DismantlingBench",
1878 Some(SpriteKind::RepairBench) => "RepairBench",
1879 None => "CraftsmanHammer",
1880 _ => "CraftsmanHammer",
1881 };
1882 Image::new(animate_by_pulse(
1883 &self
1884 .item_imgs
1885 .img_ids_or_not_found_img(ItemKey::Simple(station_img.to_string())),
1886 self.pulse,
1887 ))
1888 .w_h(25.0, 25.0)
1889 .down_from(state.ids.req_station_title, 10.0)
1890 .parent(state.ids.align_ing)
1891 .set(state.ids.req_station_img, ui);
1892
1893 let station_name = match recipe.craft_sprite {
1894 Some(SpriteKind::Anvil) => "hud-crafting-anvil",
1895 Some(SpriteKind::Cauldron) => "hud-crafting-cauldron",
1896 Some(SpriteKind::CookingPot) => "hud-crafting-cooking_pot",
1897 Some(SpriteKind::CraftingBench) => "hud-crafting-crafting_bench",
1898 Some(SpriteKind::Forge) => "hud-crafting-forge",
1899 Some(SpriteKind::Loom) => "hud-crafting-loom",
1900 Some(SpriteKind::SpinningWheel) => "hud-crafting-spinning_wheel",
1901 Some(SpriteKind::TanningRack) => "hud-crafting-tanning_rack",
1902 Some(SpriteKind::DismantlingBench) => "hud-crafting-salvaging_station",
1903 Some(SpriteKind::RepairBench) => "hud-crafting-repair_bench",
1904 _ => "",
1905 };
1906 Text::new(&self.localized_strings.get_msg(station_name))
1907 .right_from(state.ids.req_station_img, 10.0)
1908 .font_id(self.fonts.cyri.conrod_id)
1909 .font_size(self.fonts.cyri.scale(14))
1910 .color(
1911 if self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1912 == recipe.craft_sprite
1913 {
1914 TEXT_COLOR
1915 } else {
1916 TEXT_DULL_RED_COLOR
1917 },
1918 )
1919 .set(state.ids.req_station_txt, ui);
1920 }
1921 let (mut iter_a, mut iter_b, mut iter_c);
1924 let ingredients = match recipe_kind {
1925 RecipeKind::Simple => {
1926 iter_a = recipe
1927 .inputs
1928 .iter()
1929 .map(|(recipe, amount, _)| (recipe, *amount));
1930 &mut iter_a as &mut dyn ExactSizeIterator<Item = (&RecipeInput, u32)>
1931 },
1932 RecipeKind::ModularWeapon | RecipeKind::Repair => {
1933 iter_b = core::iter::empty();
1934 &mut iter_b
1935 },
1936 RecipeKind::Component(toolkind) => {
1937 if let Some(material) = craft_slot_1
1938 .and_then(|slot| match slot {
1939 Slot::Inventory(slot) => self.inventory.get(slot),
1940 Slot::Equip(_) => None,
1941 Slot::Overflow(_) => None,
1942 })
1943 .and_then(|item| item.item_definition_id().itemdef_id().map(String::from))
1944 {
1945 let component_key = ComponentKey {
1946 toolkind,
1947 material,
1948 modifier: craft_slot_2
1949 .and_then(|slot| match slot {
1950 Slot::Inventory(slot) => self.inventory.get(slot),
1951 Slot::Equip(_) => None,
1952 Slot::Overflow(_) => None,
1953 })
1954 .and_then(|item| {
1955 item.item_definition_id().itemdef_id().map(String::from)
1956 }),
1957 };
1958 if let Some(comp_recipe) =
1959 self.client.component_recipe_book().get(&component_key)
1960 {
1961 iter_c = comp_recipe.inputs();
1962 &mut iter_c as &mut dyn ExactSizeIterator<Item = _>
1963 } else {
1964 iter_b = core::iter::empty();
1965 &mut iter_b
1966 }
1967 } else {
1968 iter_b = core::iter::empty();
1969 &mut iter_b
1970 }
1971 },
1972 };
1973
1974 let num_ingredients = ingredients.len();
1975 if num_ingredients > 0 {
1976 Text::new(&self.localized_strings.get_msg("hud-crafting-ingredients"))
1977 .font_id(self.fonts.cyri.conrod_id)
1978 .font_size(self.fonts.cyri.scale(18))
1979 .color(TEXT_COLOR)
1980 .and(|t| {
1981 if recipe.craft_sprite.is_some() {
1982 t.down_from(state.ids.req_station_img, 10.0)
1983 } else {
1984 t.top_left_with_margins_on(state.ids.align_ing, 10.0, 5.0)
1985 }
1986 })
1987 .set(state.ids.ingredients_txt, ui);
1988
1989 if state.ids.ingredient_frame.len() < num_ingredients {
1991 state.update(|state| {
1992 state
1993 .ids
1994 .ingredient_frame
1995 .resize(num_ingredients, &mut ui.widget_id_generator())
1996 });
1997 };
1998 if state.ids.ingredients.len() < num_ingredients {
1999 state.update(|state| {
2000 state
2001 .ids
2002 .ingredients
2003 .resize(num_ingredients, &mut ui.widget_id_generator())
2004 });
2005 };
2006 if state.ids.ingredient_btn.len() < num_ingredients {
2007 state.update(|state| {
2008 state
2009 .ids
2010 .ingredient_btn
2011 .resize(num_ingredients, &mut ui.widget_id_generator())
2012 });
2013 };
2014 if state.ids.ingredient_img.len() < num_ingredients {
2015 state.update(|state| {
2016 state
2017 .ids
2018 .ingredient_img
2019 .resize(num_ingredients, &mut ui.widget_id_generator())
2020 });
2021 };
2022 if state.ids.req_text.len() < num_ingredients {
2023 state.update(|state| {
2024 state
2025 .ids
2026 .req_text
2027 .resize(num_ingredients, &mut ui.widget_id_generator())
2028 });
2029 };
2030
2031 for (i, (recipe_input, amount)) in ingredients.enumerate() {
2033 let item_def = match recipe_input {
2034 RecipeInput::Item(item_def) => Some(Arc::clone(item_def)),
2035 RecipeInput::Tag(tag) | RecipeInput::TagSameItem(tag) => self
2036 .inventory
2037 .slots()
2038 .find_map(|slot| {
2039 slot.as_ref().and_then(|item| {
2040 if item.matches_recipe_input(recipe_input, amount) {
2041 item.item_definition_id()
2042 .itemdef_id()
2043 .map(Arc::<ItemDef>::load_expect_cloned)
2044 } else {
2045 None
2046 }
2047 })
2048 })
2049 .or_else(|| {
2050 tag.exemplar_identifier()
2051 .map(Arc::<ItemDef>::load_expect_cloned)
2052 }),
2053 RecipeInput::ListSameItem(item_defs) => self
2054 .inventory
2055 .slots()
2056 .find_map(|slot| {
2057 slot.as_ref().and_then(|item| {
2058 if item.matches_recipe_input(recipe_input, amount) {
2059 item.item_definition_id()
2060 .itemdef_id()
2061 .map(Arc::<ItemDef>::load_expect_cloned)
2062 } else {
2063 None
2064 }
2065 })
2066 })
2067 .or_else(|| {
2068 item_defs.first().and_then(|i| {
2069 i.item_definition_id()
2070 .itemdef_id()
2071 .map(Arc::<ItemDef>::load_expect_cloned)
2072 })
2073 }),
2074 };
2075
2076 let item_def = if let Some(item_def) = item_def {
2077 item_def
2078 } else {
2079 warn!(
2080 "Failed to create example item def for recipe input {:?}",
2081 recipe_input
2082 );
2083 continue;
2084 };
2085
2086 let item_count_in_inventory = self.inventory.item_count(&item_def);
2089 let col = if item_count_in_inventory >= u64::from(amount.max(1)) {
2090 TEXT_COLOR
2091 } else {
2092 TEXT_DULL_RED_COLOR
2093 };
2094 let frame_pos = if i == 0 {
2096 state.ids.ingredients_txt
2097 } else {
2098 state.ids.ingredient_frame[i - 1]
2099 };
2100
2101 let quality_col_img = match &item_def.quality() {
2102 Quality::Low => self.imgs.inv_slot_grey,
2103 Quality::Common => self.imgs.inv_slot,
2104 Quality::Moderate => self.imgs.inv_slot_green,
2105 Quality::High => self.imgs.inv_slot_blue,
2106 Quality::Epic => self.imgs.inv_slot_purple,
2107 Quality::Legendary => self.imgs.inv_slot_gold,
2108 Quality::Artifact => self.imgs.inv_slot_orange,
2109 _ => self.imgs.inv_slot_red,
2110 };
2111 let frame = Image::new(quality_col_img).w_h(25.0, 25.0);
2112 let frame = if amount == 0 {
2113 frame.down_from(state.ids.req_text[i], 10.0)
2114 } else {
2115 frame.down_from(frame_pos, 10.0)
2116 };
2117 frame.set(state.ids.ingredient_frame[i], ui);
2118
2119 if Button::image(self.imgs.wpn_icon_border)
2121 .w_h(22.0, 22.0)
2122 .middle_of(state.ids.ingredient_frame[i])
2123 .hover_image(self.imgs.wpn_icon_border_mo)
2124 .with_item_tooltip(
2125 self.item_tooltip_manager,
2126 core::iter::once(&*item_def as &dyn ItemDesc),
2127 &None,
2128 &item_tooltip,
2129 )
2130 .set(state.ids.ingredient_btn[i], ui)
2131 .was_clicked()
2132 {
2133 events.push(Event::ChangeCraftingTab(CraftingTab::All));
2134
2135 #[expect(deprecated)]
2136 events.push(Event::SearchRecipe(Some(
2143 item_def.legacy_name().to_string(),
2144 )));
2145 }
2146
2147 Image::new(animate_by_pulse(
2149 &self.item_imgs.img_ids_or_not_found_img((&*item_def).into()),
2150 self.pulse,
2151 ))
2152 .middle_of(state.ids.ingredient_btn[i])
2153 .w_h(20.0, 20.0)
2154 .graphics_for(state.ids.ingredient_btn[i])
2155 .with_item_tooltip(
2156 self.item_tooltip_manager,
2157 core::iter::once(&*item_def as &dyn ItemDesc),
2158 &None,
2159 &item_tooltip,
2160 )
2161 .set(state.ids.ingredient_img[i], ui);
2162
2163 let over9k = "99+";
2166 let in_inv: &str = &item_count_in_inventory.to_string();
2167 if amount == 0 {
2170 let ref_widget = if i == 0 {
2172 state.ids.ingredients_txt
2173 } else {
2174 state.ids.ingredient_frame[i - 1]
2175 };
2176 Text::new(&self.localized_strings.get_msg("hud-crafting-tool_cata"))
2177 .down_from(ref_widget, 10.0)
2178 .font_id(self.fonts.cyri.conrod_id)
2179 .font_size(self.fonts.cyri.scale(18))
2180 .color(TEXT_COLOR)
2181 .set(state.ids.req_text[i], ui);
2182
2183 let (name, _) = util::item_text(
2184 item_def.as_ref(),
2185 self.localized_strings,
2186 self.item_i18n,
2187 );
2188 Text::new(&name)
2189 .right_from(state.ids.ingredient_frame[i], 10.0)
2190 .font_id(self.fonts.cyri.conrod_id)
2191 .font_size(self.fonts.cyri.scale(14))
2192 .color(col)
2193 .set(state.ids.ingredients[i], ui);
2194 } else {
2195 let name = match recipe_input {
2197 RecipeInput::Item(_) => {
2198 let (name, _) = util::item_text(
2199 item_def.as_ref(),
2200 self.localized_strings,
2201 self.item_i18n,
2202 );
2203
2204 name
2205 },
2206 RecipeInput::Tag(tag) | RecipeInput::TagSameItem(tag) => {
2207 format!("Any {} item", tag.name())
2209 },
2210 RecipeInput::ListSameItem(item_defs) => {
2211 format!(
2213 "Any of {}",
2214 item_defs
2215 .iter()
2216 .map(|def| {
2217 let (name, _) = util::item_text(
2218 def.as_ref(),
2219 self.localized_strings,
2220 self.item_i18n,
2221 );
2222
2223 name
2224 })
2225 .collect::<String>()
2226 )
2227 },
2228 };
2229 let input = format!(
2230 "{}x {} ({})",
2231 amount,
2232 name,
2233 if item_count_in_inventory > 99 {
2234 over9k
2235 } else {
2236 in_inv
2237 }
2238 );
2239 Text::new(&input)
2241 .right_from(state.ids.ingredient_frame[i], 10.0)
2242 .font_id(self.fonts.cyri.conrod_id)
2243 .font_size(self.fonts.cyri.scale(12))
2244 .color(col)
2245 .wrap_by_word()
2246 .w(150.0)
2247 .set(state.ids.ingredients[i], ui);
2248 }
2249 }
2250 }
2251 } else if *sel_crafting_tab == CraftingTab::Dismantle {
2252 Text::new(
2254 &self
2255 .localized_strings
2256 .get_msg("hud-crafting-dismantle_title"),
2257 )
2258 .mid_top_with_margin_on(state.ids.align_ing, 0.0)
2259 .font_id(self.fonts.cyri.conrod_id)
2260 .font_size(self.fonts.cyri.scale(24))
2261 .color(TEXT_COLOR)
2262 .parent(state.ids.window)
2263 .set(state.ids.dismantle_title, ui);
2264
2265 let size = 140.0;
2267 Image::new(animate_by_pulse(
2268 &self
2269 .item_imgs
2270 .img_ids_or_not_found_img(ItemKey::Simple("DismantlingBench".to_string())),
2271 self.pulse,
2272 ))
2273 .wh([size; 2])
2274 .mid_top_with_margin_on(state.ids.align_ing, 50.0)
2275 .parent(state.ids.align_ing)
2276 .set(state.ids.dismantle_img, ui);
2277
2278 Text::new(
2281 &self
2282 .localized_strings
2283 .get_msg("hud-crafting-dismantle_explanation"),
2284 )
2285 .mid_bottom_with_margin_on(state.ids.dismantle_img, -60.0)
2286 .font_id(self.fonts.cyri.conrod_id)
2287 .font_size(self.fonts.cyri.scale(14))
2288 .color(TEXT_COLOR)
2289 .parent(state.ids.window)
2290 .set(state.ids.dismantle_txt, ui);
2291 }
2292
2293 if let Some(key) = &self.show.crafting_fields.crafting_search_key {
2295 if Button::image(self.imgs.close_btn)
2296 .top_left_with_margins_on(state.ids.align_rec, -20.0, 5.0)
2297 .w_h(14.0, 14.0)
2298 .hover_image(self.imgs.close_btn_hover)
2299 .press_image(self.imgs.close_btn_press)
2300 .parent(state.ids.window)
2301 .set(state.ids.btn_close_search, ui)
2302 .was_clicked()
2303 {
2304 events.push(Event::SearchRecipe(None));
2305 }
2306 Rectangle::fill([162.0, 20.0])
2307 .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 16.0)
2308 .hsla(0.0, 0.0, 0.0, 0.7)
2309 .depth(1.0)
2310 .parent(state.ids.window)
2311 .set(state.ids.input_bg_search, ui);
2312 if let Some(string) = TextEdit::new(key.as_str())
2313 .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 18.0)
2314 .w_h(138.0, 20.0)
2315 .font_id(self.fonts.cyri.conrod_id)
2316 .font_size(self.fonts.cyri.scale(14))
2317 .color(TEXT_COLOR)
2318 .parent(state.ids.window)
2319 .set(state.ids.input_search, ui)
2320 {
2321 events.push(Event::SearchRecipe(Some(string)));
2322 }
2323 } else {
2324 Text::new(&self.localized_strings.get_msg("hud-crafting-recipes"))
2325 .mid_top_with_margin_on(state.ids.align_rec, -22.0)
2326 .font_id(self.fonts.cyri.conrod_id)
2327 .font_size(self.fonts.cyri.scale(14))
2328 .color(TEXT_COLOR)
2329 .parent(state.ids.window)
2330 .set(state.ids.title_rec, ui);
2331 Rectangle::fill_with([148.0, 20.0], color::TRANSPARENT)
2332 .top_left_with_margins_on(state.ids.window, 52.0, 26.0)
2333 .graphics_for(state.ids.btn_open_search)
2334 .set(state.ids.input_overlay_search, ui);
2335 let (eye, eye_hover, eye_press, tooltip_key) =
2336 if self.settings.gameplay.show_all_recipes {
2337 (
2338 self.imgs.eye_open_btn,
2339 self.imgs.eye_open_btn_hover,
2340 self.imgs.eye_open_btn_press,
2341 "hud-crafting-hide_unknown_recipes",
2342 )
2343 } else {
2344 (
2345 self.imgs.eye_closed_btn,
2346 self.imgs.eye_closed_btn_hover,
2347 self.imgs.eye_closed_btn_press,
2348 "hud-crafting-show_unknown_recipes",
2349 )
2350 };
2351
2352 if Button::image(eye)
2353 .top_left_with_margins_on(state.ids.align_rec, -21.0, 5.0)
2354 .w_h(16.0, 16.0)
2355 .hover_image(eye_hover)
2356 .press_image(eye_press)
2357 .parent(state.ids.window)
2358 .with_tooltip(
2359 self.tooltip_manager,
2360 &self.localized_strings.get_msg(tooltip_key),
2361 "",
2362 &tabs_tooltip,
2363 TEXT_COLOR,
2364 )
2365 .set(state.ids.btn_show_all_recipes, ui)
2366 .was_clicked()
2367 {
2368 events.push(Event::ShowAllRecipes(
2369 !self.settings.gameplay.show_all_recipes,
2370 ));
2371 }
2372 if Button::image(self.imgs.search_btn)
2373 .right_from(state.ids.btn_show_all_recipes, 5.0)
2374 .w_h(16.0, 16.0)
2375 .hover_image(self.imgs.search_btn_hover)
2376 .press_image(self.imgs.search_btn_press)
2377 .parent(state.ids.window)
2378 .set(state.ids.btn_open_search, ui)
2379 .was_clicked()
2380 {
2381 events.push(Event::SearchRecipe(Some(String::new())));
2382 events.push(Event::Focus(state.ids.input_search));
2383 }
2384 }
2385
2386 Scrollbar::y_axis(state.ids.align_rec)
2388 .thickness(5.0)
2389 .rgba(0.66, 0.66, 0.66, 1.0)
2390 .set(state.ids.scrollbar_rec, ui);
2391 Scrollbar::y_axis(state.ids.align_ing)
2392 .thickness(5.0)
2393 .rgba(0.66, 0.66, 0.66, 1.0)
2394 .set(state.ids.scrollbar_ing, ui);
2395
2396 if self
2397 .global_state
2398 .settings
2399 .interface
2400 .toggle_draggable_windows
2401 {
2402 let draggable_dim = [crafting_window_size.x, 48.0];
2404
2405 Rectangle::fill_with(draggable_dim, color::TRANSPARENT)
2406 .top_left_with_margin_on(state.ids.window_frame, 0.0)
2407 .set(state.ids.draggable_area, ui);
2408
2409 let pos_delta: Vec2<f64> = ui
2410 .widget_input(state.ids.draggable_area)
2411 .drags()
2412 .left()
2413 .map(|drag| Vec2::<f64>::from(drag.delta_xy))
2414 .sum();
2415
2416 let pos_delta = pos_delta.with_x(-pos_delta.x);
2420
2421 let crafting_window_size_with_tabs =
2422 crafting_window_size.with_x(crafting_window_size.x + 40.0);
2423 let window_clamp = Vec2::new(ui.win_w, ui.win_h) - crafting_window_size_with_tabs;
2424
2425 let new_pos = (crafting_pos + pos_delta)
2426 .map(|e| e.max(0.))
2427 .map2(window_clamp, |e, bounds| e.min(bounds));
2428
2429 if new_pos.abs_diff_ne(&crafting_pos, f64::EPSILON) {
2430 events.push(Event::MoveCrafting(new_pos));
2431 }
2432
2433 if ui
2434 .widget_input(state.ids.draggable_area)
2435 .clicks()
2436 .right()
2437 .count()
2438 == 1
2439 {
2440 events.push(Event::MoveCrafting(HudPositionSettings::default().crafting));
2441 }
2442 }
2443
2444 events
2445 }
2446}