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