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