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 fn content_contains(content: &common::comp::Content, s: &str) -> bool {
675 match content {
676 common::comp::Content::Plain(p) => p.contains(s),
677 common::comp::Content::Key(k) => k.contains(s),
678 common::comp::Content::Attr(k, _) => k.contains(s),
679 common::comp::Content::Localized { key, .. } => key.contains(s),
680 }
681 }
682 let search = |item: &Arc<ItemDef>| {
683 let (name_key, _) = item.i18n(self.item_i18n);
684 let fallback_name = self
685 .localized_strings
686 .get_content_fallback(&name_key)
687 .to_lowercase();
688 let name = self.localized_strings.get_content(&name_key).to_lowercase();
689
690 search_keys.iter().all(|&substring| {
691 name.contains(substring)
692 || fallback_name.contains(substring)
693 || content_contains(&name_key, substring)
694 })
695 };
696 let known_recipes = self
697 .inventory
698 .available_recipes_iter(self.rbm)
699 .map(|r| r.0.as_str())
700 .collect::<HashSet<_>>();
701 let recipe_source = if self.settings.gameplay.show_all_recipes {
702 Either::Left(
703 self.rbm
704 .iter()
705 .map(|r| (r, known_recipes.contains(r.0.as_str()))),
706 )
707 } else {
708 Either::Right(
709 self.inventory
710 .available_recipes_iter(self.rbm)
711 .map(|r| (r, true)),
712 )
713 };
714 let mut ordered_recipes: Vec<_> = recipe_source
715 .filter(|((_, recipe), _)| match search_filter {
716 SearchFilter::None => {
717 search(&recipe.output.0)
718 },
719 SearchFilter::Input => recipe.inputs().any(|(input, _, _)| {
720 let search_tag = |name: &str| {
721 search_keys
722 .iter()
723 .all(|&substring| name.contains(substring))
724 };
725 match input {
726 RecipeInput::Item(def) => search(def),
727 RecipeInput::Tag(tag) => search_tag(tag.name()),
728 RecipeInput::TagSameItem(tag) => search_tag(tag.name()),
729 RecipeInput::ListSameItem(defs) => {
730 defs.iter().any(search)
731 },
732 }
733 }),
734 SearchFilter::Nonexistent => false,
735 })
736 .map(|((name, recipe), known)| {
737 let has_materials = self.client.available_recipes().get(name.as_str()).is_some();
738 let is_craftable =
739 self.client
740 .available_recipes()
741 .get(name.as_str()).is_some_and(|cs| {
742 cs.is_none_or(|cs| {
743 Some(cs) == self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
744 })
745 });
746 (name, recipe, is_craftable, has_materials, known)
747 })
748 .chain(
749 pseudo_entries
750 .iter()
751 .filter(|(_, (_, _, tab))| *sel_crafting_tab == CraftingTab::All || sel_crafting_tab == tab)
753 .filter(|(_, (_, output_name, _))| {
755 match search_filter {
756 SearchFilter::None => {
757 let output_name = output_name.to_lowercase();
758 search_keys
759 .iter()
760 .all(|&substring| output_name.contains(substring))
761 },
762 SearchFilter::Input => false,
765 SearchFilter::Nonexistent => false,
766 }
767 })
768 .map(|(recipe_name, (recipe, _, _))| {
769 (
770 recipe_name,
771 *recipe,
772 self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
773 == recipe.craft_sprite,
774 true,
775 true,
776 )
777 }),
778 )
779 .collect();
780 ordered_recipes.sort_by_key(|(_, recipe, is_craftable, has_materials, known)| {
781 (
782 !known,
783 !is_craftable,
784 !has_materials,
785 recipe.output.0.quality(),
786 #[expect(deprecated)]
787 recipe.output.0.name(),
788 )
789 });
790
791 let recipe_list_length = if self.settings.gameplay.show_all_recipes {
793 self.rbm.iter().count()
794 } else {
795 self.inventory.recipe_book_len()
796 } + pseudo_entries.len();
797 if state.ids.recipe_list_btns.len() < recipe_list_length {
798 state.update(|state| {
799 state
800 .ids
801 .recipe_list_btns
802 .resize(recipe_list_length, &mut ui.widget_id_generator())
803 });
804 }
805 if state.ids.recipe_list_labels.len() < recipe_list_length {
806 state.update(|state| {
807 state
808 .ids
809 .recipe_list_labels
810 .resize(recipe_list_length, &mut ui.widget_id_generator())
811 });
812 }
813 if state.ids.recipe_list_quality_indicators.len() < recipe_list_length {
814 state.update(|state| {
815 state
816 .ids
817 .recipe_list_quality_indicators
818 .resize(recipe_list_length, &mut ui.widget_id_generator())
819 });
820 }
821 if state.ids.recipe_list_materials_indicators.len() < recipe_list_length {
822 state.update(|state| {
823 state
824 .ids
825 .recipe_list_materials_indicators
826 .resize(recipe_list_length, &mut ui.widget_id_generator())
827 });
828 }
829 for (i, (name, recipe, is_craftable, has_materials, knows_recipe)) in ordered_recipes
830 .into_iter()
831 .filter(|(_, recipe, _, _, _)| self.show.crafting_fields.crafting_tab.satisfies(recipe))
832 .enumerate()
833 {
834 let button = Button::image(if state.selected_recipe.as_ref() == Some(name) {
835 self.imgs.selection
836 } else {
837 self.imgs.nothing
838 })
839 .and(|button| {
840 if i == 0 {
841 button.top_left_with_margins_on(state.ids.align_rec, 2.0, 7.0)
842 } else {
843 button.down_from(state.ids.recipe_list_btns[i - 1], 5.0)
844 }
845 })
846 .w(171.0)
847 .hover_image(self.imgs.selection_hover)
848 .press_image(self.imgs.selection_press)
849 .image_color(color::rgba(1.0, 0.82, 0.27, 1.0));
850
851 let title;
852 let recipe_name =
853 if let Some((_recipe, pseudo_name, _filter_tab)) = pseudo_entries.get(name) {
854 pseudo_name
855 } else {
856 (title, _) = util::item_text(
857 recipe.output.0.as_ref(),
858 self.localized_strings,
859 self.item_i18n,
860 );
861 &title
862 };
863
864 let text = Text::new(recipe_name)
865 .color(if is_craftable {
866 TEXT_COLOR
867 } else {
868 TEXT_GRAY_COLOR
869 })
870 .font_size(self.fonts.cyri.scale(12))
871 .font_id(self.fonts.cyri.conrod_id)
872 .w(163.0)
873 .mid_top_with_margin_on(state.ids.recipe_list_btns[i], 3.0)
874 .graphics_for(state.ids.recipe_list_btns[i])
875 .center_justify();
876
877 let text_height = match text.get_y_dimension(ui) {
878 Dimension::Absolute(y) => y,
879 _ => 0.0,
880 };
881 let button_height = (text_height + 7.0).max(20.0);
882
883 if button
884 .h(button_height)
885 .set(state.ids.recipe_list_btns[i], ui)
886 .was_clicked()
887 {
888 if state.selected_recipe.as_ref() == Some(name) {
889 state.update(|s| s.selected_recipe = None);
890 } else {
891 if self.show.crafting_fields.crafting_tab.is_adhoc() {
892 events.push(Event::ChangeCraftingTab(CraftingTab::All));
895 }
896 state.update(|s| s.selected_recipe = Some(name.clone()));
897 }
898 events.push(Event::ClearRecipeInputs);
899 }
900 text.set(state.ids.recipe_list_labels[i], ui);
902
903 let color::Hsla(h, s, l, _) = get_quality_col(recipe.output.0.quality()).to_hsl();
905 let val_multiplier = if is_craftable { 0.7 } else { 0.5 };
906 let quality_col = color::hsl(
911 h,
912 s * val_multiplier * f32::min(l, 1.0 - l)
913 / f32::min(l * val_multiplier, 1.0 - l * val_multiplier),
914 l * val_multiplier,
915 );
916
917 Button::image(self.imgs.quality_indicator)
918 .image_color(quality_col)
919 .w_h(4.0, button_height)
920 .left_from(state.ids.recipe_list_btns[i], 1.0)
921 .graphics_for(state.ids.recipe_list_btns[i])
922 .set(state.ids.recipe_list_quality_indicators[i], ui);
923
924 if !knows_recipe {
926 let recipe_img = "Recipe";
927
928 Button::image(animate_by_pulse(
929 &self
930 .item_imgs
931 .img_ids_or_not_found_img(ItemKey::Simple(recipe_img.to_string())),
932 self.pulse,
933 ))
934 .image_color(color::LIGHT_RED)
935 .w_h(button_height - 8.0, button_height - 8.0)
936 .top_left_with_margins_on(state.ids.recipe_list_btns[i], 4.0, 4.0)
937 .graphics_for(state.ids.recipe_list_btns[i])
938 .set(state.ids.recipe_list_materials_indicators[i], ui);
939 } else if has_materials && !is_craftable {
940 let station_img = match recipe.craft_sprite {
941 Some(SpriteKind::Anvil) => Some("Anvil"),
942 Some(SpriteKind::Cauldron) => Some("Cauldron"),
943 Some(SpriteKind::CookingPot) => Some("CookingPot"),
944 Some(SpriteKind::CraftingBench) => Some("CraftingBench"),
945 Some(SpriteKind::Forge) => Some("Forge"),
946 Some(SpriteKind::Loom) => Some("Loom"),
947 Some(SpriteKind::SpinningWheel) => Some("SpinningWheel"),
948 Some(SpriteKind::TanningRack) => Some("TanningRack"),
949 Some(SpriteKind::DismantlingBench) => Some("DismantlingBench"),
950 Some(SpriteKind::RepairBench) => Some("RepairBench"),
951 _ => None,
952 };
953
954 if let Some(station_img_str) = station_img {
955 Button::image(animate_by_pulse(
956 &self
957 .item_imgs
958 .img_ids_or_not_found_img(ItemKey::Simple(station_img_str.to_string())),
959 self.pulse,
960 ))
961 .image_color(color::LIGHT_RED)
962 .w_h(button_height - 8.0, button_height - 8.0)
963 .top_left_with_margins_on(state.ids.recipe_list_btns[i], 4.0, 4.0)
964 .graphics_for(state.ids.recipe_list_btns[i])
965 .set(state.ids.recipe_list_materials_indicators[i], ui);
966 }
967 }
968 }
969
970 if self.show.crafting_fields.crafting_tab.is_adhoc() {
973 state.update(|s| s.selected_recipe = None);
974 }
975
976 if let Some((recipe_name, recipe)) = match state.selected_recipe.as_deref() {
978 Some(selected_recipe) => {
979 if let Some((modular_recipe, _pseudo_name, _filter_tab)) =
980 pseudo_entries.get(selected_recipe)
981 {
982 Some((selected_recipe, *modular_recipe))
983 } else {
984 self.inventory
985 .get_recipe(selected_recipe, self.rbm)
986 .map(|r| (selected_recipe, r))
987 }
988 },
989 None => None,
990 } {
991 let recipe_name = String::from(recipe_name);
992
993 let title;
994 let title = if let Some((_recipe, pseudo_name, _filter_tab)) =
995 pseudo_entries.get(&recipe_name)
996 {
997 pseudo_name
998 } else {
999 (title, _) = util::item_text(
1000 recipe.output.0.as_ref(),
1001 self.localized_strings,
1002 self.item_i18n,
1003 );
1004 &title
1005 };
1006
1007 Text::new(title)
1009 .mid_top_with_margin_on(state.ids.align_ing, -22.0)
1010 .font_id(self.fonts.cyri.conrod_id)
1011 .font_size(self.fonts.cyri.scale(14))
1012 .color(TEXT_COLOR)
1013 .parent(state.ids.window)
1014 .set(state.ids.title_ing, ui);
1015
1016 #[derive(Clone, Copy, Debug)]
1017 enum RecipeKind {
1018 ModularWeapon,
1019 Component(ToolKind),
1020 Simple,
1021 Repair,
1022 }
1023
1024 let recipe_kind = match recipe_name.as_str() {
1025 "veloren.core.pseudo_recipe.modular_weapon" => RecipeKind::ModularWeapon,
1026 "veloren.core.pseudo_recipe.modular_weapon_component.sword" => {
1027 RecipeKind::Component(ToolKind::Sword)
1028 },
1029 "veloren.core.pseudo_recipe.modular_weapon_component.axe" => {
1030 RecipeKind::Component(ToolKind::Axe)
1031 },
1032 "veloren.core.pseudo_recipe.modular_weapon_component.hammer" => {
1033 RecipeKind::Component(ToolKind::Hammer)
1034 },
1035 "veloren.core.pseudo_recipe.modular_weapon_component.bow" => {
1036 RecipeKind::Component(ToolKind::Bow)
1037 },
1038 "veloren.core.pseudo_recipe.modular_weapon_component.staff" => {
1039 RecipeKind::Component(ToolKind::Staff)
1040 },
1041 "veloren.core.pseudo_recipe.modular_weapon_component.sceptre" => {
1042 RecipeKind::Component(ToolKind::Sceptre)
1043 },
1044 "veloren.core.pseudo_recipe.repair" => RecipeKind::Repair,
1045 _ => RecipeKind::Simple,
1046 };
1047
1048 let mut slot_maker = SlotMaker {
1049 empty_slot: self.imgs.inv_slot,
1050 filled_slot: self.imgs.inv_slot,
1051 selected_slot: self.imgs.inv_slot_sel,
1052 background_color: Some(UI_MAIN),
1053 content_size: ContentSize {
1054 width_height_ratio: 1.0,
1055 max_fraction: 0.75,
1056 },
1057 selected_content_scale: 1.067,
1058 amount_font: self.fonts.cyri.conrod_id,
1059 amount_margins: Vec2::new(-4.0, 0.0),
1060 amount_font_size: self.fonts.cyri.scale(12),
1061 amount_text_color: TEXT_COLOR,
1062 content_source: self.inventory,
1063 image_source: self.item_imgs,
1064 slot_manager: Some(self.slot_manager),
1065 pulse: self.pulse,
1066 };
1067
1068 let (craft_slot_1, craft_slot_2, can_perform, recipe_known) = match recipe_kind {
1070 RecipeKind::ModularWeapon | RecipeKind::Component(_) => {
1071 if state.ids.craft_slots.len() < 2 {
1072 state.update(|s| {
1073 s.ids.craft_slots.resize(2, &mut ui.widget_id_generator());
1074 });
1075 }
1076
1077 Text::new(&self.localized_strings.get_msg("hud-crafting-modular_desc"))
1079 .mid_top_of(state.ids.align_ing)
1080 .w(264.0)
1081 .center_justify()
1082 .font_id(self.fonts.cyri.conrod_id)
1083 .font_size(self.fonts.cyri.scale(13))
1084 .color(TEXT_COLOR)
1085 .set(state.ids.modular_desc_txt, ui);
1086
1087 Image::new(self.imgs.crafting_modular_art)
1089 .down_from(state.ids.modular_desc_txt, 15.0)
1090 .align_middle_x()
1091 .w_h(168.0, 250.0)
1092 .set(state.ids.modular_art, ui);
1093
1094 let primary_slot = CraftSlot {
1095 index: 0,
1096 slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
1097 requirement: match recipe_kind {
1098 RecipeKind::ModularWeapon => |item, _, _| {
1099 matches!(
1100 &*item.kind(),
1101 ItemKind::ModularComponent(
1102 ModularComponent::ToolPrimaryComponent { .. }
1103 )
1104 )
1105 },
1106 RecipeKind::Component(_) => |item, comp_recipes, info| {
1107 if let Some(CraftSlotInfo::Tool(toolkind)) = info {
1108 comp_recipes
1109 .iter()
1110 .filter(|(key, _)| key.toolkind == toolkind)
1111 .any(|(key, _)| {
1112 Some(key.material.as_str())
1113 == item.item_definition_id().itemdef_id()
1114 })
1115 } else {
1116 false
1117 }
1118 },
1119 RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
1120 },
1121 info: match recipe_kind {
1122 RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
1123 RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
1124 None
1125 },
1126 },
1127 };
1128
1129 let primary_slot_widget = slot_maker
1130 .fabricate(primary_slot, [40.0; 2])
1131 .top_left_with_margins_on(state.ids.modular_art, 4.0, 4.0)
1132 .parent(state.ids.align_ing);
1133
1134 if let Some(item) = primary_slot.item(self.inventory) {
1135 primary_slot_widget
1136 .with_item_tooltip(
1137 self.item_tooltip_manager,
1138 core::iter::once(item as &dyn ItemDesc),
1139 &None,
1140 &item_tooltip,
1141 )
1142 .set(state.ids.craft_slots[0], ui);
1143 } else {
1144 let (tooltip_title, tooltip_desc) = match recipe_kind {
1145 RecipeKind::ModularWeapon => (
1146 self.localized_strings
1147 .get_msg("hud-crafting-mod_weap_prim_slot_title"),
1148 self.localized_strings
1149 .get_msg("hud-crafting-mod_weap_prim_slot_desc"),
1150 ),
1151 RecipeKind::Component(
1152 ToolKind::Sword | ToolKind::Axe | ToolKind::Hammer,
1153 ) => (
1154 self.localized_strings
1155 .get_msg("hud-crafting-mod_comp_metal_prim_slot_title"),
1156 self.localized_strings
1157 .get_msg("hud-crafting-mod_comp_metal_prim_slot_desc"),
1158 ),
1159 RecipeKind::Component(
1160 ToolKind::Bow | ToolKind::Staff | ToolKind::Sceptre,
1161 ) => (
1162 self.localized_strings
1163 .get_msg("hud-crafting-mod_comp_wood_prim_slot_title"),
1164 self.localized_strings
1165 .get_msg("hud-crafting-mod_comp_wood_prim_slot_desc"),
1166 ),
1167 RecipeKind::Component(_) | RecipeKind::Simple | RecipeKind::Repair => {
1168 (Cow::Borrowed(""), Cow::Borrowed(""))
1169 },
1170 };
1171 primary_slot_widget
1172 .with_tooltip(
1173 self.tooltip_manager,
1174 &tooltip_title,
1175 &tooltip_desc,
1176 &tabs_tooltip,
1177 TEXT_COLOR,
1178 )
1179 .set(state.ids.craft_slots[0], ui);
1180 }
1181
1182 let secondary_slot = CraftSlot {
1183 index: 1,
1184 slot: self.show.crafting_fields.recipe_inputs.get(&1).copied(),
1185 requirement: match recipe_kind {
1186 RecipeKind::ModularWeapon => |item, _, _| {
1187 matches!(
1188 &*item.kind(),
1189 ItemKind::ModularComponent(
1190 ModularComponent::ToolSecondaryComponent { .. }
1191 )
1192 )
1193 },
1194 RecipeKind::Component(_) => |item, comp_recipes, info| {
1195 if let Some(CraftSlotInfo::Tool(toolkind)) = info {
1196 comp_recipes
1197 .iter()
1198 .filter(|(key, _)| key.toolkind == toolkind)
1199 .any(|(key, _)| {
1200 key.modifier.as_deref()
1201 == item.item_definition_id().itemdef_id()
1202 })
1203 } else {
1204 false
1205 }
1206 },
1207 RecipeKind::Simple | RecipeKind::Repair => |_, _, _| unreachable!(),
1208 },
1209 info: match recipe_kind {
1210 RecipeKind::Component(toolkind) => Some(CraftSlotInfo::Tool(toolkind)),
1211 RecipeKind::ModularWeapon | RecipeKind::Simple | RecipeKind::Repair => {
1212 None
1213 },
1214 },
1215 };
1216
1217 let secondary_slot_widget = slot_maker
1218 .fabricate(secondary_slot, [40.0; 2])
1219 .top_right_with_margins_on(state.ids.modular_art, 4.0, 4.0)
1220 .parent(state.ids.align_ing);
1221
1222 if let Some(item) = secondary_slot.item(self.inventory) {
1223 secondary_slot_widget
1224 .with_item_tooltip(
1225 self.item_tooltip_manager,
1226 core::iter::once(item as &dyn ItemDesc),
1227 &None,
1228 &item_tooltip,
1229 )
1230 .set(state.ids.craft_slots[1], ui);
1231 } else {
1232 let (tooltip_title, tooltip_desc) = match recipe_kind {
1233 RecipeKind::ModularWeapon => (
1234 self.localized_strings
1235 .get_msg("hud-crafting-mod_weap_sec_slot_title"),
1236 self.localized_strings
1237 .get_msg("hud-crafting-mod_weap_sec_slot_desc"),
1238 ),
1239 RecipeKind::Component(_) => (
1240 self.localized_strings
1241 .get_msg("hud-crafting-mod_comp_sec_slot_title"),
1242 self.localized_strings
1243 .get_msg("hud-crafting-mod_comp_sec_slot_desc"),
1244 ),
1245 RecipeKind::Simple | RecipeKind::Repair => {
1246 (Cow::Borrowed(""), Cow::Borrowed(""))
1247 },
1248 };
1249 secondary_slot_widget
1250 .with_tooltip(
1251 self.tooltip_manager,
1252 &tooltip_title,
1253 &tooltip_desc,
1254 &tabs_tooltip,
1255 TEXT_COLOR,
1256 )
1257 .set(state.ids.craft_slots[1], ui);
1258 }
1259
1260 let prim_item_placed = primary_slot.slot.is_some();
1261 let sec_item_placed = secondary_slot.slot.is_some();
1262
1263 let prim_icon = match recipe_kind {
1264 RecipeKind::ModularWeapon => self.imgs.icon_primary_comp,
1265 RecipeKind::Component(ToolKind::Sword) => self.imgs.icon_ingot,
1266 RecipeKind::Component(ToolKind::Axe) => self.imgs.icon_ingot,
1267 RecipeKind::Component(ToolKind::Hammer) => self.imgs.icon_ingot,
1268 RecipeKind::Component(ToolKind::Bow) => self.imgs.icon_log,
1269 RecipeKind::Component(ToolKind::Staff) => self.imgs.icon_log,
1270 RecipeKind::Component(ToolKind::Sceptre) => self.imgs.icon_log,
1271 RecipeKind::Component(ToolKind::Shield) => self.imgs.icon_ingot,
1272 _ => self.imgs.not_found,
1273 };
1274
1275 let sec_icon = match recipe_kind {
1276 RecipeKind::ModularWeapon => self.imgs.icon_secondary_comp,
1277 RecipeKind::Component(_) => self.imgs.icon_claw,
1278 _ => self.imgs.not_found,
1279 };
1280
1281 Image::new(self.imgs.inv_slot)
1283 .w_h(80.0, 80.0)
1284 .mid_bottom_with_margin_on(state.ids.modular_art, 16.0)
1285 .parent(state.ids.align_ing)
1286 .set(state.ids.output_img_frame, ui);
1287 let bg_col = Color::Rgba(1.0, 1.0, 1.0, 0.4);
1288 if !prim_item_placed {
1289 Image::new(prim_icon)
1290 .middle_of(state.ids.craft_slots[0])
1291 .color(Some(bg_col))
1292 .w_h(34.0, 34.0)
1293 .graphics_for(state.ids.craft_slots[0])
1294 .set(state.ids.modular_wep_ing_1_bg, ui);
1295 }
1296 if !sec_item_placed {
1297 Image::new(sec_icon)
1298 .middle_of(state.ids.craft_slots[1])
1299 .color(Some(bg_col))
1300 .w_h(50.0, 50.0)
1301 .graphics_for(state.ids.craft_slots[1])
1302 .set(state.ids.modular_wep_ing_2_bg, ui);
1303 }
1304
1305 let ability_map = &AbilityMap::load().read();
1306 let msm = &MaterialStatManifest::load().read();
1307
1308 let (output_item, recipe_known) = match recipe_kind {
1309 RecipeKind::ModularWeapon => {
1310 let item = if let Some((primary_comp, toolkind, hand_restriction)) =
1311 primary_slot.item(self.inventory).and_then(|item| {
1312 if let ItemKind::ModularComponent(
1313 ModularComponent::ToolPrimaryComponent {
1314 toolkind,
1315 hand_restriction,
1316 ..
1317 },
1318 ) = &*item.kind()
1319 {
1320 Some((item, *toolkind, *hand_restriction))
1321 } else {
1322 None
1323 }
1324 }) {
1325 secondary_slot
1326 .item(self.inventory)
1327 .filter(|item| {
1328 matches!(
1329 &*item.kind(),
1330 ItemKind::ModularComponent(
1331 ModularComponent::ToolSecondaryComponent { toolkind: toolkind_b, hand_restriction: hand_restriction_b, .. }
1332 ) if toolkind == *toolkind_b && modular::compatible_handedness(hand_restriction, *hand_restriction_b)
1333 )
1334 })
1335 .map(|secondary_comp| {
1336 Item::new_from_item_base(
1337 ItemBase::Modular(modular::ModularBase::Tool),
1338 vec![
1339 primary_comp.duplicate(ability_map, msm),
1340 secondary_comp.duplicate(ability_map, msm),
1341 ],
1342 ability_map,
1343 msm,
1344 )
1345 })
1346 } else {
1347 None
1348 };
1349 (item, true)
1350 },
1351 RecipeKind::Component(toolkind) => {
1352 if let Some(material) =
1353 primary_slot.item(self.inventory).and_then(|item| {
1354 item.item_definition_id().itemdef_id().map(String::from)
1355 })
1356 {
1357 let component_key = ComponentKey {
1358 toolkind,
1359 material,
1360 modifier: secondary_slot.item(self.inventory).and_then(
1361 |item| {
1362 item.item_definition_id().itemdef_id().map(String::from)
1363 },
1364 ),
1365 };
1366 self.client
1367 .component_recipe_book()
1368 .get(&component_key)
1369 .map(|component_recipe| {
1370 let item = component_recipe.item_output(ability_map, msm);
1371 let learned = self
1372 .inventory
1373 .recipe_is_known(&component_recipe.recipe_book_key);
1374 (item, learned)
1375 })
1376 .map_or((None, true), |(item, known)| (Some(item), known))
1377 } else {
1378 (None, true)
1379 }
1380 },
1381 RecipeKind::Simple | RecipeKind::Repair => (None, true),
1382 };
1383
1384 if let Some(output_item) = output_item {
1385 let (name, _) =
1386 util::item_text(&output_item, self.localized_strings, self.item_i18n);
1387 Button::image(animate_by_pulse(
1388 &self
1389 .item_imgs
1390 .img_ids_or_not_found_img(ItemKey::from(&output_item)),
1391 self.pulse,
1392 ))
1393 .w_h(55.0, 55.0)
1394 .label(&name)
1395 .label_color(TEXT_COLOR)
1396 .label_font_size(self.fonts.cyri.scale(14))
1397 .label_font_id(self.fonts.cyri.conrod_id)
1398 .label_y(conrod_core::position::Relative::Scalar(-64.0))
1399 .label_x(conrod_core::position::Relative::Scalar(0.0))
1400 .middle_of(state.ids.output_img_frame)
1401 .with_item_tooltip(
1402 self.item_tooltip_manager,
1403 core::iter::once(&output_item as &dyn ItemDesc),
1404 &None,
1405 &item_tooltip,
1406 )
1407 .set(state.ids.output_img, ui);
1408 (
1409 primary_slot.slot,
1410 secondary_slot.slot,
1411 self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1412 == recipe.craft_sprite,
1413 recipe_known,
1414 )
1415 } else {
1416 Image::new(self.imgs.icon_mod_weap)
1417 .middle_of(state.ids.output_img_frame)
1418 .color(Some(bg_col))
1419 .w_h(70.0, 70.0)
1420 .graphics_for(state.ids.output_img)
1421 .set(state.ids.modular_wep_empty_bg, ui);
1422 (primary_slot.slot, secondary_slot.slot, false, recipe_known)
1423 }
1424 },
1425 RecipeKind::Simple => {
1426 let quality_col_img = match recipe.output.0.quality() {
1428 Quality::Low => self.imgs.inv_slot_grey,
1429 Quality::Common => self.imgs.inv_slot,
1430 Quality::Moderate => self.imgs.inv_slot_green,
1431 Quality::High => self.imgs.inv_slot_blue,
1432 Quality::Epic => self.imgs.inv_slot_purple,
1433 Quality::Legendary => self.imgs.inv_slot_gold,
1434 Quality::Artifact => self.imgs.inv_slot_orange,
1435 _ => self.imgs.inv_slot_red,
1436 };
1437
1438 Image::new(quality_col_img)
1439 .w_h(60.0, 60.0)
1440 .top_right_with_margins_on(state.ids.align_ing, 15.0, 10.0)
1441 .parent(state.ids.align_ing)
1442 .set(state.ids.output_img_frame, ui);
1443
1444 let output_text = format!("x{}", &recipe.output.1.to_string());
1445 Button::image(animate_by_pulse(
1447 &self
1448 .item_imgs
1449 .img_ids_or_not_found_img((&*recipe.output.0).into()),
1450 self.pulse,
1451 ))
1452 .w_h(55.0, 55.0)
1453 .label(&output_text)
1454 .label_color(TEXT_COLOR)
1455 .label_font_size(self.fonts.cyri.scale(14))
1456 .label_font_id(self.fonts.cyri.conrod_id)
1457 .label_y(conrod_core::position::Relative::Scalar(-24.0))
1458 .label_x(conrod_core::position::Relative::Scalar(24.0))
1459 .middle_of(state.ids.output_img_frame)
1460 .with_item_tooltip(
1461 self.item_tooltip_manager,
1462 core::iter::once(&*recipe.output.0 as &dyn ItemDesc),
1463 &None,
1464 &item_tooltip,
1465 )
1466 .set(state.ids.output_img, ui);
1467
1468 if state.ids.tags_ing.len() < CraftingTab::iter().len() {
1470 state.update(|state| {
1471 state
1472 .ids
1473 .tags_ing
1474 .resize(CraftingTab::iter().len(), &mut ui.widget_id_generator())
1475 });
1476 }
1477 for (row, chunk) in CraftingTab::iter()
1478 .filter(|crafting_tab| match crafting_tab {
1479 CraftingTab::All => false,
1480 _ => crafting_tab.satisfies(recipe),
1481 })
1482 .filter(|crafting_tab| {
1483 crafting_tab != &self.show.crafting_fields.crafting_tab
1484 })
1485 .collect::<Vec<_>>()
1486 .chunks(3)
1487 .enumerate()
1488 {
1489 for (col, crafting_tab) in chunk.iter().rev().enumerate() {
1490 let i = 3 * row + col;
1491 let icon = Image::new(crafting_tab.img_id(self.imgs))
1492 .w_h(20.0, 20.0)
1493 .parent(state.ids.window);
1494 let icon = if col == 0 {
1495 icon.bottom_right_with_margins_on(
1496 state.ids.output_img_frame,
1497 -24.0 - 24.0 * (row as f64),
1498 4.0,
1499 )
1500 } else {
1501 icon.left_from(state.ids.tags_ing[i - 1], 4.0)
1502 };
1503 icon.with_tooltip(
1504 self.tooltip_manager,
1505 &self.localized_strings.get_msg(crafting_tab.name_key()),
1506 "",
1507 &tabs_tooltip,
1508 TEXT_COLOR,
1509 )
1510 .set(state.ids.tags_ing[i], ui);
1511 }
1512 }
1513 (
1514 None,
1515 None,
1516 self.client
1517 .available_recipes()
1518 .get(&recipe_name)
1519 .is_some_and(|cs| {
1520 cs.is_none_or(|cs| {
1521 Some(cs)
1522 == self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1523 })
1524 }),
1525 true,
1526 )
1527 },
1528 RecipeKind::Repair => {
1529 if state.ids.craft_slots.is_empty() {
1530 state.update(|s| {
1531 s.ids.craft_slots.resize(1, &mut ui.widget_id_generator());
1532 });
1533 }
1534 if state.ids.repair_buttons.len() < 2 {
1535 state.update(|s| {
1536 s.ids
1537 .repair_buttons
1538 .resize(2, &mut ui.widget_id_generator());
1539 });
1540 }
1541
1542 Text::new(&self.localized_strings.get_msg("hud-crafting-repair_desc"))
1544 .mid_top_of(state.ids.align_ing)
1545 .w(264.0)
1546 .center_justify()
1547 .font_id(self.fonts.cyri.conrod_id)
1548 .font_size(self.fonts.cyri.scale(13))
1549 .color(TEXT_COLOR)
1550 .set(state.ids.modular_desc_txt, ui);
1551
1552 let repair_slot = CraftSlot {
1554 index: 0,
1555 slot: self.show.crafting_fields.recipe_inputs.get(&0).copied(),
1556 requirement: |item, _, _| item.durability_lost().is_some_and(|d| d > 0),
1557 info: None,
1558 };
1559
1560 let repair_slot_widget = slot_maker
1561 .fabricate(repair_slot, [80.0; 2])
1562 .down_from(state.ids.modular_desc_txt, 15.0)
1563 .align_middle_x()
1564 .parent(state.ids.align_ing);
1565
1566 if let Some(item) = repair_slot.item(self.inventory) {
1567 repair_slot_widget
1568 .with_item_tooltip(
1569 self.item_tooltip_manager,
1570 core::iter::once(item as &dyn ItemDesc),
1571 &None,
1572 &item_tooltip,
1573 )
1574 .set(state.ids.craft_slots[0], ui);
1575 } else {
1576 repair_slot_widget
1577 .with_tooltip(
1578 self.tooltip_manager,
1579 &self
1580 .localized_strings
1581 .get_msg("hud-crafting-repair_slot_title"),
1582 &self
1583 .localized_strings
1584 .get_msg("hud-crafting-repair_slot_desc"),
1585 &tabs_tooltip,
1586 TEXT_COLOR,
1587 )
1588 .set(state.ids.craft_slots[0], ui);
1589 }
1590
1591 if repair_slot.slot.is_none() {
1592 Image::new(self.imgs.icon_mod_weap)
1593 .middle_of(state.ids.craft_slots[0])
1594 .w_h(70.0, 70.0)
1595 .graphics_for(state.ids.craft_slots[0])
1596 .set(state.ids.modular_wep_ing_1_bg, ui);
1597 }
1598
1599 let can_repair = |item: &Item| {
1600 item.durability_lost().is_some_and(|d| d > 0)
1603 && self
1604 .client
1605 .repair_recipe_book()
1606 .repair_recipe(item)
1607 .is_some_and(|recipe| {
1608 recipe
1609 .inventory_contains_ingredients(item, self.inventory)
1610 .is_ok()
1611 })
1612 };
1613
1614 let can_perform = self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1615 == recipe.craft_sprite;
1616
1617 let color = if can_perform {
1618 TEXT_COLOR
1619 } else {
1620 TEXT_GRAY_COLOR
1621 };
1622
1623 let btn_image_hover = if can_perform {
1624 self.imgs.button_hover
1625 } else {
1626 self.imgs.button
1627 };
1628
1629 let btn_image_press = if can_perform {
1630 self.imgs.button_press
1631 } else {
1632 self.imgs.button
1633 };
1634
1635 if Button::image(self.imgs.button)
1637 .w_h(105.0, 25.0)
1638 .hover_image(btn_image_hover)
1639 .press_image(btn_image_press)
1640 .label(
1641 &self
1642 .localized_strings
1643 .get_msg("hud-crafting-repair_equipped"),
1644 )
1645 .label_y(conrod_core::position::Relative::Scalar(1.0))
1646 .label_color(color)
1647 .label_font_size(self.fonts.cyri.scale(12))
1648 .label_font_id(self.fonts.cyri.conrod_id)
1649 .image_color(color)
1650 .down_from(state.ids.craft_slots[0], 45.0)
1651 .x_relative_to(state.ids.craft_slots[0], 0.0)
1652 .set(state.ids.repair_buttons[0], ui)
1653 .was_clicked()
1654 && can_perform
1655 {
1656 self.inventory
1657 .equipped_items_with_slot()
1658 .filter(|(_, item)| can_repair(item))
1659 .for_each(|(slot, _)| {
1660 events.push(Event::RepairItem {
1661 slot: Slot::Equip(slot),
1662 });
1663 })
1664 }
1665
1666 if Button::image(self.imgs.button)
1668 .w_h(105.0, 25.0)
1669 .hover_image(btn_image_hover)
1670 .press_image(btn_image_press)
1671 .label(&self.localized_strings.get_msg("hud-crafting-repair_all"))
1672 .label_y(conrod_core::position::Relative::Scalar(1.0))
1673 .label_color(color)
1674 .label_font_size(self.fonts.cyri.scale(12))
1675 .label_font_id(self.fonts.cyri.conrod_id)
1676 .image_color(color)
1677 .down_from(state.ids.repair_buttons[0], 5.0)
1678 .x_relative_to(state.ids.craft_slots[0], 0.0)
1679 .set(state.ids.repair_buttons[1], ui)
1680 .was_clicked()
1681 && can_perform
1682 {
1683 self.inventory
1684 .equipped_items_with_slot()
1685 .filter(|(_, item)| can_repair(item))
1686 .for_each(|(slot, _)| {
1687 events.push(Event::RepairItem {
1688 slot: Slot::Equip(slot),
1689 });
1690 });
1691 self.inventory
1692 .slots_with_id()
1693 .filter(|(_, item)| item.as_ref().is_some_and(can_repair))
1694 .for_each(|(slot, _)| {
1695 events.push(Event::RepairItem {
1696 slot: Slot::Inventory(slot),
1697 });
1698 });
1699 }
1700
1701 let can_perform =
1702 repair_slot.item(self.inventory).is_some_and(can_repair) && can_perform;
1703
1704 (repair_slot.slot, None, can_perform, true)
1705 },
1706 };
1707
1708 Line::centred([0.0, 0.0], [274.0, 0.0])
1710 .color(color::rgba(1.0, 1.0, 1.0, 0.1))
1711 .down_from(state.ids.align_ing, 0.0)
1712 .parent(state.ids.window)
1713 .set(state.ids.btn_separator, ui);
1714
1715 let label = &match recipe_kind {
1717 RecipeKind::Repair => self
1718 .localized_strings
1719 .get_msg("hud-crafting-repair-selection"),
1720 _ => self.localized_strings.get_msg("hud-crafting-craft"),
1721 };
1722 let craft_button_init = Button::image(self.imgs.button)
1723 .w_h(105.0, 25.0)
1724 .hover_image(if can_perform {
1725 self.imgs.button_hover
1726 } else {
1727 self.imgs.button
1728 })
1729 .press_image(if can_perform {
1730 self.imgs.button_press
1731 } else {
1732 self.imgs.button
1733 })
1734 .label(label)
1735 .label_y(conrod_core::position::Relative::Scalar(1.0))
1736 .label_color(if can_perform {
1737 TEXT_COLOR
1738 } else {
1739 TEXT_GRAY_COLOR
1740 })
1741 .label_font_size(self.fonts.cyri.scale(12))
1742 .label_font_id(self.fonts.cyri.conrod_id)
1743 .image_color(if can_perform {
1744 TEXT_COLOR
1745 } else {
1746 TEXT_GRAY_COLOR
1747 })
1748 .and(|b| match recipe_kind {
1749 RecipeKind::Repair => b
1750 .down_from(state.ids.craft_slots[0], 15.0)
1751 .x_relative_to(state.ids.craft_slots[0], 0.0)
1752 .parent(state.ids.align_ing),
1753 _ => b
1754 .bottom_left_with_margins_on(state.ids.align_ing, -31.0, 10.0)
1755 .parent(state.ids.window_frame),
1756 });
1757
1758 let craft_button = if !recipe_known {
1759 craft_button_init
1760 .with_tooltip(
1761 self.tooltip_manager,
1762 &self
1763 .localized_strings
1764 .get_msg("hud-crafting-recipe-uncraftable"),
1765 &self
1766 .localized_strings
1767 .get_msg("hud-crafting-recipe-unlearned"),
1768 &tabs_tooltip,
1769 TEXT_COLOR,
1770 )
1771 .set(state.ids.btn_craft, ui)
1772 } else {
1773 craft_button_init.set(state.ids.btn_craft, ui)
1774 };
1775
1776 if craft_button.was_clicked() && can_perform {
1777 match recipe_kind {
1778 RecipeKind::ModularWeapon => {
1779 if let (
1780 Some(Slot::Inventory(primary_slot)),
1781 Some(Slot::Inventory(secondary_slot)),
1782 ) = (craft_slot_1, craft_slot_2)
1783 {
1784 events.push(Event::CraftModularWeapon {
1785 primary_slot,
1786 secondary_slot,
1787 });
1788 }
1789 },
1790 RecipeKind::Component(toolkind) => {
1791 if let Some(Slot::Inventory(primary_slot)) = craft_slot_1 {
1792 events.push(Event::CraftModularWeaponComponent {
1793 toolkind,
1794 material: primary_slot,
1795 modifier: craft_slot_2.and_then(|slot| match slot {
1796 Slot::Inventory(slot) => Some(slot),
1797 Slot::Equip(_) => None,
1798 Slot::Overflow(_) => None,
1799 }),
1800 });
1801 }
1802 },
1803 RecipeKind::Simple => events.push(Event::CraftRecipe {
1804 recipe_name,
1805 amount: 1,
1806 }),
1807 RecipeKind::Repair => {
1808 if let Some(slot) = craft_slot_1 {
1809 events.push(Event::RepairItem { slot });
1810 }
1811 },
1812 }
1813 }
1814
1815 if matches!(recipe_kind, RecipeKind::Simple)
1817 && Button::image(self.imgs.button)
1818 .w_h(105.0, 25.0)
1819 .hover_image(if can_perform {
1820 self.imgs.button_hover
1821 } else {
1822 self.imgs.button
1823 })
1824 .press_image(if can_perform {
1825 self.imgs.button_press
1826 } else {
1827 self.imgs.button
1828 })
1829 .label(&self.localized_strings.get_msg("hud-crafting-craft_all"))
1830 .label_y(conrod_core::position::Relative::Scalar(1.0))
1831 .label_color(if can_perform {
1832 TEXT_COLOR
1833 } else {
1834 TEXT_GRAY_COLOR
1835 })
1836 .label_font_size(self.fonts.cyri.scale(12))
1837 .label_font_id(self.fonts.cyri.conrod_id)
1838 .image_color(if can_perform {
1839 TEXT_COLOR
1840 } else {
1841 TEXT_GRAY_COLOR
1842 })
1843 .bottom_right_with_margins_on(state.ids.align_ing, -31.0, 10.0)
1844 .parent(state.ids.window_frame)
1845 .set(state.ids.btn_craft_all, ui)
1846 .was_clicked()
1847 && can_perform
1848 {
1849 if let Some(selected_recipe) = &state.selected_recipe {
1850 let amount = recipe.max_from_ingredients(self.inventory);
1851 if amount > 0 {
1852 events.push(Event::CraftRecipe {
1853 recipe_name: selected_recipe.to_string(),
1854 amount,
1855 });
1856 }
1857 } else {
1858 error!("State shows no selected recipe when trying to craft multiple.");
1859 }
1860 };
1861
1862 if recipe.craft_sprite.is_some() {
1864 Text::new(
1865 &self
1866 .localized_strings
1867 .get_msg("hud-crafting-req_crafting_station"),
1868 )
1869 .font_id(self.fonts.cyri.conrod_id)
1870 .font_size(self.fonts.cyri.scale(18))
1871 .color(TEXT_COLOR)
1872 .and(|t| match recipe_kind {
1873 RecipeKind::Simple => {
1874 t.top_left_with_margins_on(state.ids.align_ing, 10.0, 5.0)
1875 },
1876 RecipeKind::ModularWeapon | RecipeKind::Component(_) => t
1877 .down_from(state.ids.modular_art, 25.0)
1878 .x_place_on(state.ids.align_ing, Place::Start(Some(5.0))),
1879 RecipeKind::Repair => t
1880 .down_from(state.ids.repair_buttons[1], 20.0)
1881 .x_place_on(state.ids.align_ing, Place::Start(Some(5.0))),
1882 })
1883 .set(state.ids.req_station_title, ui);
1884 let station_img = match recipe.craft_sprite {
1885 Some(SpriteKind::Anvil) => "Anvil",
1886 Some(SpriteKind::Cauldron) => "Cauldron",
1887 Some(SpriteKind::CookingPot) => "CookingPot",
1888 Some(SpriteKind::CraftingBench) => "CraftingBench",
1889 Some(SpriteKind::Forge) => "Forge",
1890 Some(SpriteKind::Loom) => "Loom",
1891 Some(SpriteKind::SpinningWheel) => "SpinningWheel",
1892 Some(SpriteKind::TanningRack) => "TanningRack",
1893 Some(SpriteKind::DismantlingBench) => "DismantlingBench",
1894 Some(SpriteKind::RepairBench) => "RepairBench",
1895 None => "CraftsmanHammer",
1896 _ => "CraftsmanHammer",
1897 };
1898 Image::new(animate_by_pulse(
1899 &self
1900 .item_imgs
1901 .img_ids_or_not_found_img(ItemKey::Simple(station_img.to_string())),
1902 self.pulse,
1903 ))
1904 .w_h(25.0, 25.0)
1905 .down_from(state.ids.req_station_title, 10.0)
1906 .parent(state.ids.align_ing)
1907 .set(state.ids.req_station_img, ui);
1908
1909 let station_name = match recipe.craft_sprite {
1910 Some(SpriteKind::Anvil) => "hud-crafting-anvil",
1911 Some(SpriteKind::Cauldron) => "hud-crafting-cauldron",
1912 Some(SpriteKind::CookingPot) => "hud-crafting-cooking_pot",
1913 Some(SpriteKind::CraftingBench) => "hud-crafting-crafting_bench",
1914 Some(SpriteKind::Forge) => "hud-crafting-forge",
1915 Some(SpriteKind::Loom) => "hud-crafting-loom",
1916 Some(SpriteKind::SpinningWheel) => "hud-crafting-spinning_wheel",
1917 Some(SpriteKind::TanningRack) => "hud-crafting-tanning_rack",
1918 Some(SpriteKind::DismantlingBench) => "hud-crafting-salvaging_station",
1919 Some(SpriteKind::RepairBench) => "hud-crafting-repair_bench",
1920 _ => "",
1921 };
1922 Text::new(&self.localized_strings.get_msg(station_name))
1923 .right_from(state.ids.req_station_img, 10.0)
1924 .font_id(self.fonts.cyri.conrod_id)
1925 .font_size(self.fonts.cyri.scale(14))
1926 .color(
1927 if self.show.crafting_fields.craft_sprite.map(|(_, s)| s)
1928 == recipe.craft_sprite
1929 {
1930 TEXT_COLOR
1931 } else {
1932 TEXT_DULL_RED_COLOR
1933 },
1934 )
1935 .set(state.ids.req_station_txt, ui);
1936 }
1937 let (mut iter_a, mut iter_b, mut iter_c, mut iter_d);
1940 let ingredients = match recipe_kind {
1941 RecipeKind::Simple => {
1942 iter_a = recipe
1943 .inputs
1944 .iter()
1945 .map(|(recipe, amount, _)| (recipe, *amount));
1946 &mut iter_a as &mut dyn ExactSizeIterator<Item = (&RecipeInput, u32)>
1947 },
1948 RecipeKind::ModularWeapon => {
1949 iter_b = core::iter::empty();
1950 &mut iter_b
1951 },
1952 RecipeKind::Component(toolkind) => {
1953 if let Some(material) = craft_slot_1
1954 .and_then(|slot| match slot {
1955 Slot::Inventory(slot) => self.inventory.get(slot),
1956 Slot::Equip(_) => None,
1957 Slot::Overflow(_) => None,
1958 })
1959 .and_then(|item| item.item_definition_id().itemdef_id().map(String::from))
1960 {
1961 let component_key = ComponentKey {
1962 toolkind,
1963 material,
1964 modifier: craft_slot_2
1965 .and_then(|slot| match slot {
1966 Slot::Inventory(slot) => self.inventory.get(slot),
1967 Slot::Equip(_) => None,
1968 Slot::Overflow(_) => None,
1969 })
1970 .and_then(|item| {
1971 item.item_definition_id().itemdef_id().map(String::from)
1972 }),
1973 };
1974 if let Some(comp_recipe) =
1975 self.client.component_recipe_book().get(&component_key)
1976 {
1977 iter_c = comp_recipe.inputs();
1978 &mut iter_c as &mut dyn ExactSizeIterator<Item = _>
1979 } else {
1980 iter_b = core::iter::empty();
1981 &mut iter_b
1982 }
1983 } else {
1984 iter_b = core::iter::empty();
1985 &mut iter_b
1986 }
1987 },
1988 RecipeKind::Repair => {
1989 if let Some(item) = match craft_slot_1 {
1990 Some(Slot::Inventory(slot)) => self.inventory.get(slot),
1991 Some(Slot::Equip(slot)) => self.inventory.equipped(slot),
1992 Some(Slot::Overflow(_)) => None,
1993 None => None,
1994 } {
1995 if let Some(recipe) = self.client.repair_recipe_book().repair_recipe(item) {
1996 iter_d = recipe.inputs(item).collect::<Vec<_>>().into_iter();
1997 &mut iter_d as &mut dyn ExactSizeIterator<Item = _>
1998 } else {
1999 iter_b = core::iter::empty();
2000 &mut iter_b
2001 }
2002 } else {
2003 iter_b = core::iter::empty();
2004 &mut iter_b
2005 }
2006 },
2007 };
2008
2009 let num_ingredients = ingredients.len();
2010 if num_ingredients > 0 {
2011 Text::new(&self.localized_strings.get_msg("hud-crafting-ingredients"))
2012 .font_id(self.fonts.cyri.conrod_id)
2013 .font_size(self.fonts.cyri.scale(18))
2014 .color(TEXT_COLOR)
2015 .and(|t| {
2016 if recipe.craft_sprite.is_some() {
2017 t.down_from(state.ids.req_station_img, 10.0)
2018 } else {
2019 t.top_left_with_margins_on(state.ids.align_ing, 10.0, 5.0)
2020 }
2021 })
2022 .set(state.ids.ingredients_txt, ui);
2023
2024 if state.ids.ingredient_frame.len() < num_ingredients {
2026 state.update(|state| {
2027 state
2028 .ids
2029 .ingredient_frame
2030 .resize(num_ingredients, &mut ui.widget_id_generator())
2031 });
2032 };
2033 if state.ids.ingredients.len() < num_ingredients {
2034 state.update(|state| {
2035 state
2036 .ids
2037 .ingredients
2038 .resize(num_ingredients, &mut ui.widget_id_generator())
2039 });
2040 };
2041 if state.ids.ingredient_btn.len() < num_ingredients {
2042 state.update(|state| {
2043 state
2044 .ids
2045 .ingredient_btn
2046 .resize(num_ingredients, &mut ui.widget_id_generator())
2047 });
2048 };
2049 if state.ids.ingredient_img.len() < num_ingredients {
2050 state.update(|state| {
2051 state
2052 .ids
2053 .ingredient_img
2054 .resize(num_ingredients, &mut ui.widget_id_generator())
2055 });
2056 };
2057 if state.ids.req_text.len() < num_ingredients {
2058 state.update(|state| {
2059 state
2060 .ids
2061 .req_text
2062 .resize(num_ingredients, &mut ui.widget_id_generator())
2063 });
2064 };
2065
2066 for (i, (recipe_input, amount)) in ingredients.enumerate() {
2068 let item_def = match recipe_input {
2069 RecipeInput::Item(item_def) => Some(Arc::clone(item_def)),
2070 RecipeInput::Tag(tag) | RecipeInput::TagSameItem(tag) => self
2071 .inventory
2072 .slots()
2073 .find_map(|slot| {
2074 slot.as_ref().and_then(|item| {
2075 if item.matches_recipe_input(recipe_input, amount) {
2076 item.item_definition_id()
2077 .itemdef_id()
2078 .map(Arc::<ItemDef>::load_expect_cloned)
2079 } else {
2080 None
2081 }
2082 })
2083 })
2084 .or_else(|| {
2085 tag.exemplar_identifier()
2086 .map(Arc::<ItemDef>::load_expect_cloned)
2087 }),
2088 RecipeInput::ListSameItem(item_defs) => self
2089 .inventory
2090 .slots()
2091 .find_map(|slot| {
2092 slot.as_ref().and_then(|item| {
2093 if item.matches_recipe_input(recipe_input, amount) {
2094 item.item_definition_id()
2095 .itemdef_id()
2096 .map(Arc::<ItemDef>::load_expect_cloned)
2097 } else {
2098 None
2099 }
2100 })
2101 })
2102 .or_else(|| {
2103 item_defs.first().and_then(|i| {
2104 i.item_definition_id()
2105 .itemdef_id()
2106 .map(Arc::<ItemDef>::load_expect_cloned)
2107 })
2108 }),
2109 };
2110
2111 let item_def = if let Some(item_def) = item_def {
2112 item_def
2113 } else {
2114 warn!(
2115 "Failed to create example item def for recipe input {:?}",
2116 recipe_input
2117 );
2118 continue;
2119 };
2120
2121 let item_count_in_inventory = self.inventory.item_count(&item_def);
2124 let col = if item_count_in_inventory >= u64::from(amount.max(1)) {
2125 TEXT_COLOR
2126 } else {
2127 TEXT_DULL_RED_COLOR
2128 };
2129 let frame_pos = if i == 0 {
2131 state.ids.ingredients_txt
2132 } else {
2133 state.ids.ingredient_frame[i - 1]
2134 };
2135
2136 let quality_col_img = match &item_def.quality() {
2137 Quality::Low => self.imgs.inv_slot_grey,
2138 Quality::Common => self.imgs.inv_slot,
2139 Quality::Moderate => self.imgs.inv_slot_green,
2140 Quality::High => self.imgs.inv_slot_blue,
2141 Quality::Epic => self.imgs.inv_slot_purple,
2142 Quality::Legendary => self.imgs.inv_slot_gold,
2143 Quality::Artifact => self.imgs.inv_slot_orange,
2144 _ => self.imgs.inv_slot_red,
2145 };
2146 let frame = Image::new(quality_col_img).w_h(25.0, 25.0);
2147 let frame = if amount == 0 {
2148 frame.down_from(state.ids.req_text[i], 10.0)
2149 } else {
2150 frame.down_from(frame_pos, 10.0)
2151 };
2152 frame.set(state.ids.ingredient_frame[i], ui);
2153 if Button::image(self.imgs.wpn_icon_border)
2155 .w_h(22.0, 22.0)
2156 .middle_of(state.ids.ingredient_frame[i])
2157 .hover_image(self.imgs.wpn_icon_border_mo)
2158 .with_item_tooltip(
2159 self.item_tooltip_manager,
2160 core::iter::once(&*item_def as &dyn ItemDesc),
2161 &None,
2162 &item_tooltip,
2163 )
2164 .set(state.ids.ingredient_btn[i], ui)
2165 .was_clicked()
2166 {
2167 events.push(Event::ChangeCraftingTab(CraftingTab::All));
2168 #[expect(deprecated)]
2169 events.push(Event::SearchRecipe(Some(item_def.name().to_string())));
2170 }
2171 Image::new(animate_by_pulse(
2173 &self.item_imgs.img_ids_or_not_found_img((&*item_def).into()),
2174 self.pulse,
2175 ))
2176 .middle_of(state.ids.ingredient_btn[i])
2177 .w_h(20.0, 20.0)
2178 .graphics_for(state.ids.ingredient_btn[i])
2179 .with_item_tooltip(
2180 self.item_tooltip_manager,
2181 core::iter::once(&*item_def as &dyn ItemDesc),
2182 &None,
2183 &item_tooltip,
2184 )
2185 .set(state.ids.ingredient_img[i], ui);
2186
2187 let over9k = "99+";
2190 let in_inv: &str = &item_count_in_inventory.to_string();
2191 if amount == 0 {
2194 let ref_widget = if i == 0 {
2196 state.ids.ingredients_txt
2197 } else {
2198 state.ids.ingredient_frame[i - 1]
2199 };
2200 Text::new(&self.localized_strings.get_msg("hud-crafting-tool_cata"))
2201 .down_from(ref_widget, 10.0)
2202 .font_id(self.fonts.cyri.conrod_id)
2203 .font_size(self.fonts.cyri.scale(18))
2204 .color(TEXT_COLOR)
2205 .set(state.ids.req_text[i], ui);
2206
2207 let (name, _) = util::item_text(
2208 item_def.as_ref(),
2209 self.localized_strings,
2210 self.item_i18n,
2211 );
2212 Text::new(&name)
2213 .right_from(state.ids.ingredient_frame[i], 10.0)
2214 .font_id(self.fonts.cyri.conrod_id)
2215 .font_size(self.fonts.cyri.scale(14))
2216 .color(col)
2217 .set(state.ids.ingredients[i], ui);
2218 } else {
2219 let name = match recipe_input {
2221 RecipeInput::Item(_) => {
2222 let (name, _) = util::item_text(
2223 item_def.as_ref(),
2224 self.localized_strings,
2225 self.item_i18n,
2226 );
2227
2228 name
2229 },
2230 RecipeInput::Tag(tag) | RecipeInput::TagSameItem(tag) => {
2231 format!("Any {} item", tag.name())
2233 },
2234 RecipeInput::ListSameItem(item_defs) => {
2235 format!(
2237 "Any of {}",
2238 item_defs
2239 .iter()
2240 .map(|def| {
2241 let (name, _) = util::item_text(
2242 def.as_ref(),
2243 self.localized_strings,
2244 self.item_i18n,
2245 );
2246
2247 name
2248 })
2249 .collect::<String>()
2250 )
2251 },
2252 };
2253 let input = format!(
2254 "{}x {} ({})",
2255 amount,
2256 name,
2257 if item_count_in_inventory > 99 {
2258 over9k
2259 } else {
2260 in_inv
2261 }
2262 );
2263 Text::new(&input)
2265 .right_from(state.ids.ingredient_frame[i], 10.0)
2266 .font_id(self.fonts.cyri.conrod_id)
2267 .font_size(self.fonts.cyri.scale(12))
2268 .color(col)
2269 .wrap_by_word()
2270 .w(150.0)
2271 .set(state.ids.ingredients[i], ui);
2272 }
2273 }
2274 }
2275 } else if *sel_crafting_tab == CraftingTab::Dismantle {
2276 Text::new(
2278 &self
2279 .localized_strings
2280 .get_msg("hud-crafting-dismantle_title"),
2281 )
2282 .mid_top_with_margin_on(state.ids.align_ing, 0.0)
2283 .font_id(self.fonts.cyri.conrod_id)
2284 .font_size(self.fonts.cyri.scale(24))
2285 .color(TEXT_COLOR)
2286 .parent(state.ids.window)
2287 .set(state.ids.dismantle_title, ui);
2288
2289 let size = 140.0;
2291 Image::new(animate_by_pulse(
2292 &self
2293 .item_imgs
2294 .img_ids_or_not_found_img(ItemKey::Simple("DismantlingBench".to_string())),
2295 self.pulse,
2296 ))
2297 .wh([size; 2])
2298 .mid_top_with_margin_on(state.ids.align_ing, 50.0)
2299 .parent(state.ids.align_ing)
2300 .set(state.ids.dismantle_img, ui);
2301
2302 Text::new(
2305 &self
2306 .localized_strings
2307 .get_msg("hud-crafting-dismantle_explanation"),
2308 )
2309 .mid_bottom_with_margin_on(state.ids.dismantle_img, -60.0)
2310 .font_id(self.fonts.cyri.conrod_id)
2311 .font_size(self.fonts.cyri.scale(14))
2312 .color(TEXT_COLOR)
2313 .parent(state.ids.window)
2314 .set(state.ids.dismantle_txt, ui);
2315 }
2316
2317 if let Some(key) = &self.show.crafting_fields.crafting_search_key {
2319 if Button::image(self.imgs.close_btn)
2320 .top_left_with_margins_on(state.ids.align_rec, -20.0, 5.0)
2321 .w_h(14.0, 14.0)
2322 .hover_image(self.imgs.close_btn_hover)
2323 .press_image(self.imgs.close_btn_press)
2324 .parent(state.ids.window)
2325 .set(state.ids.btn_close_search, ui)
2326 .was_clicked()
2327 {
2328 events.push(Event::SearchRecipe(None));
2329 }
2330 Rectangle::fill([162.0, 20.0])
2331 .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 16.0)
2332 .hsla(0.0, 0.0, 0.0, 0.7)
2333 .depth(1.0)
2334 .parent(state.ids.window)
2335 .set(state.ids.input_bg_search, ui);
2336 if let Some(string) = TextEdit::new(key.as_str())
2337 .top_left_with_margins_on(state.ids.btn_close_search, -2.0, 18.0)
2338 .w_h(138.0, 20.0)
2339 .font_id(self.fonts.cyri.conrod_id)
2340 .font_size(self.fonts.cyri.scale(14))
2341 .color(TEXT_COLOR)
2342 .parent(state.ids.window)
2343 .set(state.ids.input_search, ui)
2344 {
2345 events.push(Event::SearchRecipe(Some(string)));
2346 }
2347 } else {
2348 Text::new(&self.localized_strings.get_msg("hud-crafting-recipes"))
2349 .mid_top_with_margin_on(state.ids.align_rec, -22.0)
2350 .font_id(self.fonts.cyri.conrod_id)
2351 .font_size(self.fonts.cyri.scale(14))
2352 .color(TEXT_COLOR)
2353 .parent(state.ids.window)
2354 .set(state.ids.title_rec, ui);
2355 Rectangle::fill_with([148.0, 20.0], color::TRANSPARENT)
2356 .top_left_with_margins_on(state.ids.window, 52.0, 26.0)
2357 .graphics_for(state.ids.btn_open_search)
2358 .set(state.ids.input_overlay_search, ui);
2359 let (eye, eye_hover, eye_press, tooltip_key) =
2360 if self.settings.gameplay.show_all_recipes {
2361 (
2362 self.imgs.eye_open_btn,
2363 self.imgs.eye_open_btn_hover,
2364 self.imgs.eye_open_btn_press,
2365 "hud-crafting-hide_unknown_recipes",
2366 )
2367 } else {
2368 (
2369 self.imgs.eye_closed_btn,
2370 self.imgs.eye_closed_btn_hover,
2371 self.imgs.eye_closed_btn_press,
2372 "hud-crafting-show_unknown_recipes",
2373 )
2374 };
2375
2376 if Button::image(eye)
2377 .top_left_with_margins_on(state.ids.align_rec, -21.0, 5.0)
2378 .w_h(16.0, 16.0)
2379 .hover_image(eye_hover)
2380 .press_image(eye_press)
2381 .parent(state.ids.window)
2382 .with_tooltip(
2383 self.tooltip_manager,
2384 &self.localized_strings.get_msg(tooltip_key),
2385 "",
2386 &tabs_tooltip,
2387 TEXT_COLOR,
2388 )
2389 .set(state.ids.btn_show_all_recipes, ui)
2390 .was_clicked()
2391 {
2392 events.push(Event::ShowAllRecipes(
2393 !self.settings.gameplay.show_all_recipes,
2394 ));
2395 }
2396 if Button::image(self.imgs.search_btn)
2397 .right_from(state.ids.btn_show_all_recipes, 5.0)
2398 .w_h(16.0, 16.0)
2399 .hover_image(self.imgs.search_btn_hover)
2400 .press_image(self.imgs.search_btn_press)
2401 .parent(state.ids.window)
2402 .set(state.ids.btn_open_search, ui)
2403 .was_clicked()
2404 {
2405 events.push(Event::SearchRecipe(Some(String::new())));
2406 events.push(Event::Focus(state.ids.input_search));
2407 }
2408 }
2409
2410 Scrollbar::y_axis(state.ids.align_rec)
2412 .thickness(5.0)
2413 .rgba(0.66, 0.66, 0.66, 1.0)
2414 .set(state.ids.scrollbar_rec, ui);
2415 Scrollbar::y_axis(state.ids.align_ing)
2416 .thickness(5.0)
2417 .rgba(0.66, 0.66, 0.66, 1.0)
2418 .set(state.ids.scrollbar_ing, ui);
2419
2420 if self
2421 .global_state
2422 .settings
2423 .interface
2424 .toggle_draggable_windows
2425 {
2426 let draggable_dim = [crafting_window_size.x, 48.0];
2428
2429 Rectangle::fill_with(draggable_dim, color::TRANSPARENT)
2430 .top_left_with_margin_on(state.ids.window_frame, 0.0)
2431 .set(state.ids.draggable_area, ui);
2432
2433 let pos_delta: Vec2<f64> = ui
2434 .widget_input(state.ids.draggable_area)
2435 .drags()
2436 .left()
2437 .map(|drag| Vec2::<f64>::from(drag.delta_xy))
2438 .sum();
2439
2440 let pos_delta = pos_delta.with_x(-pos_delta.x);
2444
2445 let crafting_window_size_with_tabs =
2446 crafting_window_size.with_x(crafting_window_size.x + 40.0);
2447 let window_clamp = Vec2::new(ui.win_w, ui.win_h) - crafting_window_size_with_tabs;
2448
2449 let new_pos = (crafting_pos + pos_delta)
2450 .map(|e| e.max(0.))
2451 .map2(window_clamp, |e, bounds| e.min(bounds));
2452
2453 if new_pos.abs_diff_ne(&crafting_pos, f64::EPSILON) {
2454 events.push(Event::MoveCrafting(new_pos));
2455 }
2456
2457 if ui
2458 .widget_input(state.ids.draggable_area)
2459 .clicks()
2460 .right()
2461 .count()
2462 == 1
2463 {
2464 events.push(Event::MoveCrafting(HudPositionSettings::default().crafting));
2465 }
2466 }
2467
2468 events
2469 }
2470}