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