veloren_voxygen/hud/
crafting.rs

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