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