1use super::image_frame::ImageFrame;
2use crate::hud::{
3 HudInfo, get_quality_col,
4 img_ids::Imgs,
5 item_imgs::{ItemImgs, animate_by_pulse},
6 util,
7};
8use client::Client;
9use common::{
10 comp::{
11 Energy, Inventory,
12 item::{
13 Item, ItemDesc, ItemI18n, ItemKind, ItemTag, MaterialStatManifest, Quality,
14 armor::Protection, item_key::ItemKey, modular::ModularComponent,
15 },
16 },
17 recipe::RecipeBookManifest,
18 trade::SitePrices,
19};
20use conrod_core::{
21 Color, Colorable, FontSize, Positionable, Scalar, Sizeable, Ui, UiCell, Widget, WidgetCommon,
22 WidgetStyle, builder_method, builder_methods, image, input::global::Global,
23 position::Dimension, text, widget, widget_ids,
24};
25use i18n::Localization;
26use lazy_static::lazy_static;
27use std::{
28 borrow::Borrow,
29 time::{Duration, Instant},
30};
31
32#[derive(Copy, Clone)]
33struct Hover(widget::Id, [f64; 2]);
34#[derive(Copy, Clone)]
35enum HoverState {
36 Hovering(Hover),
37 Fading(Instant, Hover, Option<(Instant, widget::Id)>),
38 Start(Instant, widget::Id),
39 None,
40}
41
42const MOUSE_PAD_Y: f64 = 15.0;
44
45pub struct ItemTooltipManager {
46 state: HoverState,
47 hover_dur: Duration,
49 fade_dur: Duration,
51 logical_scale_factor: f64,
53 tooltip_ids: widget::id::List,
55}
56
57impl ItemTooltipManager {
58 pub fn new(hover_dur: Duration, fade_dur: Duration, logical_scale_factor: f64) -> Self {
59 Self {
60 state: HoverState::None,
61 hover_dur,
62 fade_dur,
63 logical_scale_factor,
64 tooltip_ids: widget::id::List::new(),
65 }
66 }
67
68 pub fn maintain(&mut self, input: &Global, logical_scale_factor: f64) {
69 self.logical_scale_factor = logical_scale_factor;
70
71 let current = &input.current;
72
73 if let Some(um_id) = current.widget_under_mouse {
74 match self.state {
75 HoverState::Hovering(hover) if um_id == hover.0 => (),
76 HoverState::Hovering(hover) => {
77 self.state =
78 HoverState::Fading(Instant::now(), hover, Some((Instant::now(), um_id)))
79 },
80 HoverState::Fading(_, _, Some((_, id))) if um_id == id => {},
81 HoverState::Fading(start, hover, _) => {
82 self.state = HoverState::Fading(start, hover, Some((Instant::now(), um_id)))
83 },
84 HoverState::Start(_, id) if um_id == id => (),
85 HoverState::Start(_, _) | HoverState::None => {
86 self.state = HoverState::Start(Instant::now(), um_id)
87 },
88 }
89 } else {
90 match self.state {
91 HoverState::Hovering(hover) => {
92 self.state = HoverState::Fading(Instant::now(), hover, None)
93 },
94 HoverState::Fading(start, hover, Some((_, _))) => {
95 self.state = HoverState::Fading(start, hover, None)
96 },
97 HoverState::Start(_, _) => self.state = HoverState::None,
98 HoverState::Fading(_, _, None) | HoverState::None => (),
99 }
100 }
101
102 if let HoverState::Fading(start, _, maybe_hover) = self.state {
104 if start.elapsed() > self.fade_dur {
105 self.state = match maybe_hover {
106 Some((start, hover)) => HoverState::Start(start, hover),
107 None => HoverState::None,
108 };
109 }
110 }
111 }
112
113 fn set_tooltip<'a, I>(
114 &mut self,
115 tooltip: &'a ItemTooltip,
116 items: impl Iterator<Item = I>,
117 prices: &'a Option<SitePrices>,
118 img_id: Option<image::Id>,
119 image_dims: Option<(f64, f64)>,
120 src_id: widget::Id,
121 ui: &mut UiCell,
122 ) where
123 I: Borrow<dyn ItemDesc>,
124 {
125 let mp_h = MOUSE_PAD_Y / self.logical_scale_factor;
126
127 let mut id_walker = self.tooltip_ids.walk();
128
129 let tooltip = |transparency, mouse_pos: [f64; 2], ui: &mut UiCell| {
130 let mut prev_id = None;
131 for item in items {
132 let tooltip_id =
133 id_walker.next(&mut self.tooltip_ids, &mut ui.widget_id_generator());
134 let tooltip = tooltip
137 .clone()
138 .item(item.borrow())
139 .prices(prices)
140 .image(img_id)
141 .image_dims(image_dims);
142
143 let [t_w, t_h] = tooltip.get_wh(ui).unwrap_or([0.0, 0.0]);
144 let [m_x, m_y] = [mouse_pos[0], mouse_pos[1]];
145 let (w_w, w_h) = (ui.win_w, ui.win_h);
146
147 let x = if (m_x + w_w / 2.0) > t_w {
150 m_x - t_w / 2.0
151 } else {
152 m_x + t_w / 2.0
153 };
154 let y = if w_h - (m_y + w_h / 2.0) > t_h + mp_h {
155 m_y + mp_h + t_h / 2.0
156 } else {
157 m_y - mp_h - t_h / 2.0
158 };
159
160 if let Some(prev_id) = prev_id {
161 tooltip
162 .floating(true)
163 .transparency(transparency)
164 .up_from(prev_id, 5.0)
165 .set(tooltip_id, ui);
166 } else {
167 tooltip
168 .floating(true)
169 .transparency(transparency)
170 .x_y(x, y)
171 .set(tooltip_id, ui);
172 }
173
174 prev_id = Some(tooltip_id);
175 }
176 };
177 match self.state {
178 HoverState::Hovering(Hover(id, xy)) if id == src_id => tooltip(1.0, xy, ui),
179 HoverState::Fading(start, Hover(id, xy), _) if id == src_id => tooltip(
180 (0.1f32 - start.elapsed().as_millis() as f32 / self.hover_dur.as_millis() as f32)
181 .max(0.0),
182 xy,
183 ui,
184 ),
185 HoverState::Start(start, id) if id == src_id && start.elapsed() > self.hover_dur => {
186 let xy = ui.global_input().current.mouse.xy;
187 self.state = HoverState::Hovering(Hover(id, xy));
188 tooltip(1.0, xy, ui);
189 },
190 _ => (),
191 }
192 }
193}
194
195pub struct ItemTooltipped<'a, W, I> {
196 inner: W,
197 tooltip_manager: &'a mut ItemTooltipManager,
198
199 items: I,
200 prices: &'a Option<SitePrices>,
201 img_id: Option<image::Id>,
202 image_dims: Option<(f64, f64)>,
203 tooltip: &'a ItemTooltip<'a>,
204}
205
206impl<W: Widget, I: Iterator> ItemTooltipped<'_, W, I> {
207 pub fn tooltip_image(mut self, img_id: image::Id) -> Self {
208 self.img_id = Some(img_id);
209 self
210 }
211
212 pub fn tooltip_image_dims(mut self, dims: (f64, f64)) -> Self {
213 self.image_dims = Some(dims);
214 self
215 }
216
217 pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event
218 where
219 <I as Iterator>::Item: Borrow<dyn ItemDesc>,
220 {
221 let event = self.inner.set(id, ui);
222 self.tooltip_manager.set_tooltip(
223 self.tooltip,
224 self.items,
225 self.prices,
226 self.img_id,
227 self.image_dims,
228 id,
229 ui,
230 );
231 event
232 }
233}
234
235pub trait ItemTooltipable {
236 fn with_item_tooltip<'a, I>(
238 self,
239 tooltip_manager: &'a mut ItemTooltipManager,
240
241 items: I,
242
243 prices: &'a Option<SitePrices>,
244
245 tooltip: &'a ItemTooltip<'a>,
246 ) -> ItemTooltipped<'a, Self, I>
247 where
248 Self: Sized;
249}
250impl<W: Widget> ItemTooltipable for W {
251 fn with_item_tooltip<'a, I>(
252 self,
253 tooltip_manager: &'a mut ItemTooltipManager,
254 items: I,
255 prices: &'a Option<SitePrices>,
256 tooltip: &'a ItemTooltip<'a>,
257 ) -> ItemTooltipped<'a, W, I> {
258 ItemTooltipped {
259 inner: self,
260 tooltip_manager,
261 items,
262 prices,
263 img_id: None,
264 image_dims: None,
265 tooltip,
266 }
267 }
268}
269
270const V_PAD: f64 = 10.0;
272const H_PAD: f64 = 10.0;
274const V_PAD_STATS: f64 = 6.0;
276const IMAGE_W_FRAC: f64 = 0.3;
278const ICON_SIZE: [f64; 2] = [64.0, 64.0];
280const WIDTH: f64 = 320.0;
282
283#[derive(Clone, WidgetCommon)]
285pub struct ItemTooltip<'a> {
286 #[conrod(common_builder)]
287 common: widget::CommonBuilder,
288 item: &'a dyn ItemDesc,
289 msm: &'a MaterialStatManifest,
290 rbm: &'a RecipeBookManifest,
291 inventory: Option<&'a Inventory>,
294 prices: &'a Option<SitePrices>,
295 image: Option<image::Id>,
296 image_dims: Option<(f64, f64)>,
297 style: Style,
298 transparency: f32,
299 image_frame: ImageFrame,
300 client: &'a Client,
301 info: &'a HudInfo,
302 imgs: &'a Imgs,
303 item_imgs: &'a ItemImgs,
304 pulse: f32,
305 localized_strings: &'a Localization,
306 item_i18n: &'a ItemI18n,
307}
308
309#[derive(Clone, Debug, Default, PartialEq, WidgetStyle)]
310pub struct Style {
311 #[conrod(default = "Color::Rgba(1.0, 1.0, 1.0, 1.0)")]
312 pub color: Option<Color>,
313 title: widget::text::Style,
314 desc: widget::text::Style,
315 }
317
318widget_ids! {
319 struct Ids {
320 title,
321 quantity,
322 subtitle,
323 desc,
324 prices_buy,
325 prices_sell,
326 tooltip_hints,
327 stats[],
328 diffs[],
329 item_frame,
330 item_render,
331 image_frame,
332 image,
333 background,
334 }
335}
336
337pub struct State {
338 ids: Ids,
339}
340
341lazy_static! {
342 static ref EMPTY_ITEM: Item = Item::new_from_asset_expect("common.items.weapons.empty.empty");
343}
344
345impl<'a> ItemTooltip<'a> {
346 builder_methods! {
347 pub desc_text_color { style.desc.color = Some(Color) }
348 pub title_font_size { style.title.font_size = Some(FontSize) }
349 pub desc_font_size { style.desc.font_size = Some(FontSize) }
350 pub title_justify { style.title.justify = Some(text::Justify) }
351 pub desc_justify { style.desc.justify = Some(text::Justify) }
352 pub title_line_spacing { style.title.line_spacing = Some(Scalar) }
353 pub desc_line_spacing { style.desc.line_spacing = Some(Scalar) }
354 image { image = Option<image::Id> }
355 item { item = &'a dyn ItemDesc }
356 prices { prices = &'a Option<SitePrices> }
357 msm { msm = &'a MaterialStatManifest }
358 rbm { rbm = &'a RecipeBookManifest }
359 image_dims { image_dims = Option<(f64, f64)> }
360 transparency { transparency = f32 }
361 }
362
363 pub fn new(
364 image_frame: ImageFrame,
365 client: &'a Client,
366 info: &'a HudInfo,
367 imgs: &'a Imgs,
368 item_imgs: &'a ItemImgs,
369 pulse: f32,
370 msm: &'a MaterialStatManifest,
371 rbm: &'a RecipeBookManifest,
372 inventory: Option<&'a Inventory>,
373 localized_strings: &'a Localization,
374 item_i18n: &'a ItemI18n,
375 ) -> Self {
376 ItemTooltip {
377 common: widget::CommonBuilder::default(),
378 style: Style::default(),
379 item: &*EMPTY_ITEM,
380 msm,
381 rbm,
382 inventory,
383 prices: &None,
384 transparency: 1.0,
385 image_frame,
386 image: None,
387 image_dims: None,
388 client,
389 info,
390 imgs,
391 item_imgs,
392 pulse,
393 localized_strings,
394 item_i18n,
395 }
396 }
397
398 fn text_image_width(&self, total_width: f64) -> (f64, f64) {
414 let inner_width = (total_width - H_PAD * 2.0).max(0.0);
415 let image_w = if self.image.is_some() {
417 match self.image_dims {
418 Some((w, _)) => w,
419 None => (inner_width - H_PAD).max(0.0) * IMAGE_W_FRAC,
420 }
421 } else {
422 0.0
423 };
424 let text_w = (inner_width
426 - if self.image.is_some() {
427 image_w + H_PAD
428 } else {
429 0.0
430 })
431 .max(0.0);
432
433 (text_w, image_w)
434 }
435
436 #[must_use]
438 pub fn font_id(mut self, font_id: text::font::Id) -> Self {
439 self.style.title.font_id = Some(Some(font_id));
440 self.style.desc.font_id = Some(Some(font_id));
441 self
442 }
443}
444
445impl Widget for ItemTooltip<'_> {
446 type Event = ();
447 type State = State;
448 type Style = Style;
449
450 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
451 State {
452 ids: Ids::new(id_gen),
453 }
454 }
455
456 fn style(&self) -> Self::Style { self.style.clone() }
457
458 fn update(self, args: widget::UpdateArgs<Self>) {
459 let widget::UpdateArgs {
460 id,
461 state,
462 rect,
463 ui,
464 ..
465 } = args;
466
467 let i18n = &self.localized_strings;
468 let item_i18n = &self.item_i18n;
469
470 let inventories = self.client.inventories();
471 let inventory = match inventories.get(self.info.viewpoint_entity) {
472 Some(l) => l,
473 None => return,
474 };
475
476 let item = self.item;
477
478 let quality = get_quality_col(item.quality());
479
480 let item_kind = &*item.kind();
481
482 let equipped_item = inventory.equipped_items_replaceable_by(item_kind).next();
483
484 let (title, desc) = util::item_text(item, i18n, item_i18n);
485
486 let item_kind = util::kind_text(item_kind, i18n).to_string();
487
488 let material = item.tags().into_iter().find_map(|t| match t {
489 ItemTag::MaterialKind(material) => Some(material),
490 _ => None,
491 });
492
493 let subtitle = if let Some(material) = material {
494 format!(
495 "{} ({})",
496 item_kind,
497 util::material_kind_text(&material, i18n)
498 )
499 } else {
500 item_kind
501 };
502
503 let style = self.style.desc;
504
505 let text_color = conrod_core::color::WHITE;
506
507 let (text_w, image_w) = self.text_image_width(rect.w());
509
510 let quality_col_img = match &item.quality() {
512 Quality::Low => self.imgs.inv_slot_grey,
513 Quality::Common => self.imgs.inv_slot_common,
514 Quality::Moderate => self.imgs.inv_slot_green,
515 Quality::High => self.imgs.inv_slot_blue,
516 Quality::Epic => self.imgs.inv_slot_purple,
517 Quality::Legendary => self.imgs.inv_slot_gold,
518 Quality::Artifact => self.imgs.inv_slot_orange,
519 _ => self.imgs.inv_slot_red,
520 };
521
522 let stats_count = util::stats_count(item, self.msm);
523
524 state.update(|s| {
526 s.ids
527 .stats
528 .resize(stats_count, &mut ui.widget_id_generator())
529 });
530
531 state.update(|s| {
532 s.ids
533 .diffs
534 .resize(stats_count, &mut ui.widget_id_generator())
535 });
536
537 self.image_frame
539 .wh(rect.dim())
540 .xy(rect.xy())
541 .graphics_for(id)
542 .parent(id)
543 .color(quality)
544 .set(state.ids.image_frame, ui);
545
546 if let Some(img_id) = self.image {
548 widget::Image::new(img_id)
549 .w_h(image_w, self.image_dims.map_or(image_w, |(_, h)| h))
550 .graphics_for(id)
551 .parent(id)
552 .color(Some(quality))
553 .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
554 .set(state.ids.image, ui);
555 }
556
557 widget::Image::new(quality_col_img)
559 .wh(ICON_SIZE)
560 .graphics_for(id)
561 .parent(id)
562 .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
563 .set(state.ids.item_frame, ui);
564
565 widget::Image::new(animate_by_pulse(
567 &self
568 .item_imgs
569 .img_ids_or_not_found_img(ItemKey::from(&item)),
570 self.pulse,
571 ))
572 .color(Some(conrod_core::color::WHITE))
573 .w_h(ICON_SIZE[0] * 0.8, ICON_SIZE[1] * 0.8)
574 .middle_of(state.ids.item_frame)
575 .set(state.ids.item_render, ui);
576
577 let title_w = (text_w - H_PAD * 3.0 - ICON_SIZE[0]).max(0.0);
578
579 widget::Text::new(&title)
581 .w(title_w)
582 .graphics_for(id)
583 .parent(id)
584 .with_style(self.style.title)
585 .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD)
586 .right_from(state.ids.item_frame, H_PAD)
587 .color(quality)
588 .set(state.ids.title, ui);
589
590 let amount = i18n.get_msg_ctx(
591 "items-common-amount",
592 &i18n::fluent_args! { "amount" => self.item.amount().get() },
593 );
594
595 let (subtitle_relative_id, spacing) = if self.item.amount().get() > 1 {
597 widget::Text::new(&amount)
598 .w(title_w)
599 .graphics_for(id)
600 .parent(id)
601 .with_style(self.style.desc)
602 .color(conrod_core::color::GREY)
603 .down_from(state.ids.title, 2.0)
604 .set(state.ids.quantity, ui);
605
606 (state.ids.quantity, 2.0)
607 } else {
608 (state.ids.title, V_PAD)
609 };
610
611 widget::Text::new(&subtitle)
613 .w(title_w)
614 .graphics_for(id)
615 .parent(id)
616 .with_style(self.style.desc)
617 .color(conrod_core::color::GREY)
618 .down_from(subtitle_relative_id, spacing)
619 .set(state.ids.subtitle, ui);
620
621 match &*item.kind() {
623 ItemKind::Tool(tool) => {
624 let stats = tool.stats(item.stats_durability_multiplier());
625
626 widget::Text::new(&format!(
628 "{} : {:.1}",
629 i18n.get_msg("common-stats-power"),
630 stats.power * 10.0
631 ))
632 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
633 .graphics_for(id)
634 .parent(id)
635 .with_style(self.style.desc)
636 .color(text_color)
637 .down_from(state.ids.item_frame, V_PAD)
638 .set(state.ids.stats[0], ui);
639
640 let mut stat_text = |text: String, i: usize| {
641 widget::Text::new(&text)
642 .graphics_for(id)
643 .parent(id)
644 .with_style(self.style.desc)
645 .color(text_color)
646 .down_from(state.ids.stats[i - 1], V_PAD_STATS)
647 .set(state.ids.stats[i], ui);
648 };
649
650 stat_text(
652 format!(
653 "{} : {:+.0}%",
654 i18n.get_msg("common-stats-speed"),
655 (stats.speed - 1.0) * 100.0
656 ),
657 1,
658 );
659
660 stat_text(
662 format!(
663 "{} : {:+.0}%",
664 i18n.get_msg("common-stats-effect-power"),
665 (stats.effect_power - 1.0) * 100.0
666 ),
667 2,
668 );
669
670 stat_text(
672 format!(
673 "{} : {:+.0}%",
674 i18n.get_msg("common-stats-range"),
675 (stats.range - 1.0) * 100.0
676 ),
677 3,
678 );
679
680 stat_text(
682 format!(
683 "{} : {:+.0}%",
684 i18n.get_msg("common-stats-energy_efficiency"),
685 (stats.energy_efficiency - 1.0) * 100.0
686 ),
687 4,
688 );
689
690 stat_text(
692 format!(
693 "{} : {:+.0}%",
694 i18n.get_msg("common-stats-buff_strength"),
695 (stats.buff_strength - 1.0) * 100.0
696 ),
697 5,
698 );
699
700 if item.has_durability() {
701 let durability = Item::MAX_DURABILITY - item.durability_lost().unwrap_or(0);
702 stat_text(
703 format!(
704 "{} : {}/{}",
705 i18n.get_msg("common-stats-durability"),
706 durability,
707 Item::MAX_DURABILITY
708 ),
709 6,
710 )
711 }
712
713 if let Some(equipped_item) = equipped_item {
714 if let ItemKind::Tool(equipped_tool) = &*equipped_item.kind() {
715 let tool_stats = tool.stats(item.stats_durability_multiplier());
716 let equipped_tool_stats =
717 equipped_tool.stats(equipped_item.stats_durability_multiplier());
718 let diff = tool_stats - equipped_tool_stats;
719 let power_diff =
720 util::comparison(tool_stats.power, equipped_tool_stats.power);
721 let speed_diff =
722 util::comparison(tool_stats.speed, equipped_tool_stats.speed);
723 let effect_power_diff = util::comparison(
724 tool_stats.effect_power,
725 equipped_tool_stats.effect_power,
726 );
727 let range_diff =
728 util::comparison(tool_stats.range, equipped_tool_stats.range);
729 let energy_efficiency_diff = util::comparison(
730 tool_stats.energy_efficiency,
731 equipped_tool_stats.energy_efficiency,
732 );
733 let buff_strength_diff = util::comparison(
734 tool_stats.buff_strength,
735 equipped_tool_stats.buff_strength,
736 );
737
738 let tool_durability =
739 util::item_durability(item).unwrap_or(Item::MAX_DURABILITY);
740 let equipped_durability =
741 util::item_durability(equipped_item).unwrap_or(Item::MAX_DURABILITY);
742 let durability_diff =
743 util::comparison(tool_durability, equipped_durability);
744
745 let mut diff_text = |text: String, color, id_index| {
746 widget::Text::new(&text)
747 .align_middle_y_of(state.ids.stats[id_index])
748 .right_from(state.ids.stats[id_index], H_PAD)
749 .graphics_for(id)
750 .parent(id)
751 .with_style(style)
752 .color(color)
753 .set(state.ids.diffs[id_index], ui)
754 };
755
756 if diff.power.abs() > f32::EPSILON {
757 let text = format!("{} {:.1}", &power_diff.0, &diff.power * 10.0);
758 diff_text(text, power_diff.1, 0)
759 }
760 if diff.speed.abs() > f32::EPSILON {
761 let text = format!("{} {:+.0}", &speed_diff.0, &diff.speed * 100.0);
762 diff_text(text, speed_diff.1, 1)
763 }
764 if diff.effect_power.abs() > f32::EPSILON {
765 let text = format!(
766 "{} {:+.0}",
767 &effect_power_diff.0,
768 &diff.effect_power * 100.0
769 );
770 diff_text(text, effect_power_diff.1, 2)
771 }
772 if diff.range.abs() > f32::EPSILON {
773 let text = format!("{} {:.1}%", &range_diff.0, &diff.range * 100.0);
774 diff_text(text, range_diff.1, 3)
775 }
776 if diff.energy_efficiency.abs() > f32::EPSILON {
777 let text = format!(
778 "{} {:.1}%",
779 &energy_efficiency_diff.0,
780 &diff.energy_efficiency * 100.0
781 );
782 diff_text(text, energy_efficiency_diff.1, 4)
783 }
784 if diff.buff_strength.abs() > f32::EPSILON {
785 let text = format!(
786 "{} {:.1}%",
787 &buff_strength_diff.0,
788 &diff.buff_strength * 100.0
789 );
790 diff_text(text, buff_strength_diff.1, 5)
791 }
792 if tool_durability != equipped_durability && item.has_durability() {
793 let text = format!(
794 "{} {}",
795 &durability_diff.0,
796 tool_durability as i32 - equipped_durability as i32
797 );
798 diff_text(text, durability_diff.1, 6)
799 }
800 }
801 }
802 },
803 ItemKind::Armor(armor) => {
804 let armor_stats = armor.stats(self.msm, item.stats_durability_multiplier());
805
806 let mut stat_text = |text: String, i: usize| {
807 widget::Text::new(&text)
808 .graphics_for(id)
809 .parent(id)
810 .with_style(self.style.desc)
811 .color(text_color)
812 .and(|t| {
813 if i == 0 {
814 t.x_align_to(
815 state.ids.item_frame,
816 conrod_core::position::Align::Start,
817 )
818 .down_from(state.ids.item_frame, V_PAD)
819 } else {
820 t.down_from(state.ids.stats[i - 1], V_PAD_STATS)
821 }
822 })
823 .set(state.ids.stats[i], ui);
824 };
825
826 let mut index = 0;
827
828 if armor_stats.protection.is_some() {
829 stat_text(
830 format!(
831 "{} : {}",
832 i18n.get_msg("common-stats-armor"),
833 util::protec2string(
834 armor_stats.protection.unwrap_or(Protection::Normal(0.0))
835 )
836 ),
837 index,
838 );
839 index += 1;
840 }
841
842 if armor_stats.poise_resilience.is_some() {
844 stat_text(
845 format!(
846 "{} : {}",
847 i18n.get_msg("common-stats-poise_res"),
848 util::protec2string(
849 armor_stats
850 .poise_resilience
851 .unwrap_or(Protection::Normal(0.0))
852 )
853 ),
854 index,
855 );
856 index += 1;
857 }
858
859 if armor_stats.energy_max.is_some() {
861 stat_text(
862 format!(
863 "{} : {:.1}",
864 i18n.get_msg("common-stats-energy_max"),
865 armor_stats.energy_max.unwrap_or(0.0)
866 ),
867 index,
868 );
869 index += 1;
870 }
871
872 if armor_stats.energy_reward.is_some() {
874 stat_text(
875 format!(
876 "{} : {:.1}%",
877 i18n.get_msg("common-stats-energy_reward"),
878 armor_stats.energy_reward.map_or(0.0, |x| x * 100.0)
879 ),
880 index,
881 );
882 index += 1;
883 }
884
885 if armor_stats.precision_power.is_some() {
887 stat_text(
888 format!(
889 "{} : {:.3}",
890 i18n.get_msg("common-stats-precision_power"),
891 armor_stats.precision_power.unwrap_or(0.0)
892 ),
893 index,
894 );
895 index += 1;
896 }
897
898 if armor_stats.stealth.is_some() {
900 stat_text(
901 format!(
902 "{} : {:.3}",
903 i18n.get_msg("common-stats-stealth"),
904 armor_stats.stealth.unwrap_or(0.0)
905 ),
906 index,
907 );
908 index += 1;
909 }
910
911 if item.num_slots() > 0 {
913 stat_text(
914 format!(
915 "{} : {}",
916 i18n.get_msg("common-stats-slots"),
917 item.num_slots()
918 ),
919 index,
920 );
921 index += 1;
922 }
923
924 if let Some(durability) = util::item_durability(item) {
925 stat_text(
926 format!(
927 "{} : {}/{}",
928 i18n.get_msg("common-stats-durability"),
929 durability,
930 Item::MAX_DURABILITY
931 ),
932 index,
933 );
934 }
935
936 if let Some(equipped_item) = equipped_item {
937 if let ItemKind::Armor(equipped_armor) = &*equipped_item.kind() {
938 let equipped_stats = equipped_armor
939 .stats(self.msm, equipped_item.stats_durability_multiplier());
940 let diff = armor_stats - equipped_stats;
941 let protection_diff = util::option_comparison(
942 &armor_stats.protection,
943 &equipped_stats.protection,
944 );
945 let poise_res_diff = util::option_comparison(
946 &armor_stats.poise_resilience,
947 &equipped_stats.poise_resilience,
948 );
949 let energy_max_diff = util::option_comparison(
950 &armor_stats.energy_max,
951 &equipped_stats.energy_max,
952 );
953 let energy_reward_diff = util::option_comparison(
954 &armor_stats.energy_reward,
955 &equipped_stats.energy_reward,
956 );
957 let precision_power_diff = util::option_comparison(
958 &armor_stats.precision_power,
959 &equipped_stats.precision_power,
960 );
961 let stealth_diff =
962 util::option_comparison(&armor_stats.stealth, &equipped_stats.stealth);
963
964 let armor_durability = util::item_durability(item);
965 let equipped_durability = util::item_durability(equipped_item);
966 let durability_diff =
967 util::option_comparison(&armor_durability, &equipped_durability);
968
969 let mut diff_text = |text: String, color, id_index| {
970 widget::Text::new(&text)
971 .align_middle_y_of(state.ids.stats[id_index])
972 .right_from(state.ids.stats[id_index], H_PAD)
973 .graphics_for(id)
974 .parent(id)
975 .with_style(style)
976 .color(color)
977 .set(state.ids.diffs[id_index], ui)
978 };
979
980 let mut index = 0;
981 if let Some(p_diff) = diff.protection {
982 if p_diff != Protection::Normal(0.0) {
983 let text = format!(
984 "{} {}",
985 &protection_diff.0,
986 util::protec2string(p_diff)
987 );
988 diff_text(text, protection_diff.1, index);
989 }
990 }
991 index += armor_stats.protection.is_some() as usize;
992
993 if let Some(p_r_diff) = diff.poise_resilience {
994 if p_r_diff != Protection::Normal(0.0) {
995 let text = format!(
996 "{} {}",
997 &poise_res_diff.0,
998 util::protec2string(p_r_diff)
999 );
1000 diff_text(text, poise_res_diff.1, index);
1001 }
1002 }
1003 index += armor_stats.poise_resilience.is_some() as usize;
1004
1005 if let Some(e_m_diff) = diff.energy_max {
1006 if e_m_diff.abs() > Energy::ENERGY_EPSILON {
1007 let text = format!("{} {:.1}", &energy_max_diff.0, e_m_diff);
1008 diff_text(text, energy_max_diff.1, index);
1009 }
1010 }
1011 index += armor_stats.energy_max.is_some() as usize;
1012
1013 if let Some(e_r_diff) = diff.energy_reward {
1014 if e_r_diff.abs() > Energy::ENERGY_EPSILON {
1015 let text =
1016 format!("{} {:.1}", &energy_reward_diff.0, e_r_diff * 100.0);
1017 diff_text(text, energy_reward_diff.1, index);
1018 }
1019 }
1020 index += armor_stats.energy_reward.is_some() as usize;
1021
1022 if let Some(p_p_diff) = diff.precision_power {
1023 if p_p_diff != 0.0_f32 {
1024 let text = format!("{} {:.3}", &precision_power_diff.0, p_p_diff);
1025 diff_text(text, precision_power_diff.1, index);
1026 }
1027 }
1028 index += armor_stats.precision_power.is_some() as usize;
1029
1030 if let Some(s_diff) = diff.stealth {
1031 if s_diff != 0.0_f32 {
1032 let text = format!("{} {:.3}", &stealth_diff.0, s_diff);
1033 diff_text(text, stealth_diff.1, index);
1034 }
1035 }
1036 index += armor_stats.stealth.is_some() as usize;
1037
1038 if armor_durability != equipped_durability && item.has_durability() {
1039 let diff = armor_durability.unwrap_or(Item::MAX_DURABILITY) as i32
1040 - equipped_durability.unwrap_or(Item::MAX_DURABILITY) as i32;
1041 let text = format!("{} {}", &durability_diff.0, diff);
1042 diff_text(text, durability_diff.1, index);
1043 }
1044 }
1045 }
1046 },
1047 ItemKind::Consumable { effects, .. } => {
1048 for (i, desc) in util::consumable_desc(effects, i18n).iter().enumerate() {
1049 let (down_from, pad) = match i {
1050 0 => (state.ids.item_frame, V_PAD),
1051 _ => (state.ids.stats[i - 1], V_PAD_STATS),
1052 };
1053 widget::Text::new(desc)
1054 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1055 .graphics_for(id)
1056 .parent(id)
1057 .with_style(self.style.desc)
1058 .color(text_color)
1059 .down_from(down_from, pad)
1060 .set(state.ids.stats[i], ui);
1061 }
1062 },
1063 ItemKind::RecipeGroup { recipes } => {
1064 const KNOWN_COLOR: Color = Color::Rgba(0.0, 1.0, 0.0, 1.0);
1065 const NOT_KNOWN_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
1066 let recipe_known = self.inventory.map(|inv| {
1067 inv.recipe_groups_iter()
1068 .any(|group| group.item_definition_id() == item.item_definition_id())
1069 });
1070 let known_key_color = match recipe_known {
1071 Some(false) => Some(("items-common-recipe-not_known", NOT_KNOWN_COLOR)),
1072 Some(true) => Some(("items-common-recipe-known", KNOWN_COLOR)),
1073 None => None,
1074 };
1075 if let Some((recipe_known_key, recipe_known_color)) = known_key_color {
1076 let recipe_known_text = self.localized_strings.get_msg(recipe_known_key);
1077 widget::Text::new(&recipe_known_text)
1078 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1079 .graphics_for(id)
1080 .parent(id)
1081 .with_style(self.style.desc)
1082 .color(recipe_known_color)
1083 .down_from(state.ids.item_frame, V_PAD)
1084 .set(state.ids.stats[0], ui);
1085 }
1086 let offset = if known_key_color.is_some() { 1 } else { 0 };
1087 for (i, desc) in recipes.iter().enumerate().map(|(i, s)| (i + offset, s)) {
1088 if let Some(recipe) = self.rbm.get(desc) {
1089 let (item_name, _) = util::item_text(
1090 recipe.output.0.as_ref(),
1091 self.localized_strings,
1092 self.item_i18n,
1093 );
1094 let (down_from, pad) = match i {
1095 0 => (state.ids.item_frame, V_PAD),
1096 _ => (state.ids.stats[i - 1], V_PAD_STATS),
1097 };
1098 widget::Text::new(&item_name)
1099 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1100 .graphics_for(id)
1101 .parent(id)
1102 .with_style(self.style.desc)
1103 .color(text_color)
1104 .down_from(down_from, pad)
1105 .set(state.ids.stats[i], ui);
1106 }
1107 }
1108 },
1109 ItemKind::ModularComponent(mc) => {
1110 if let Some(stats) = mc.tool_stats(item.components(), self.msm) {
1111 let is_primary = matches!(mc, ModularComponent::ToolPrimaryComponent { .. });
1112
1113 let power_text = if is_primary {
1115 format!(
1116 "{} : {:.1}",
1117 i18n.get_msg("common-stats-power"),
1118 stats.power * 10.0
1119 )
1120 } else {
1121 format!(
1122 "{} : x{:.2}",
1123 i18n.get_msg("common-stats-power"),
1124 stats.power
1125 )
1126 };
1127 widget::Text::new(&power_text)
1128 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1129 .graphics_for(id)
1130 .parent(id)
1131 .with_style(self.style.desc)
1132 .color(text_color)
1133 .down_from(state.ids.item_frame, V_PAD)
1134 .set(state.ids.stats[0], ui);
1135
1136 let speed_text = if is_primary {
1138 format!(
1139 "{} : {:+.0}%",
1140 i18n.get_msg("common-stats-speed"),
1141 (stats.speed - 1.0) * 100.0
1142 )
1143 } else {
1144 format!(
1145 "{} : x{:.2}",
1146 i18n.get_msg("common-stats-speed"),
1147 stats.speed
1148 )
1149 };
1150 widget::Text::new(&speed_text)
1151 .graphics_for(id)
1152 .parent(id)
1153 .with_style(self.style.desc)
1154 .color(text_color)
1155 .down_from(state.ids.stats[0], V_PAD_STATS)
1156 .set(state.ids.stats[1], ui);
1157
1158 let effect_power_text = if is_primary {
1162 format!(
1163 "{} : {:+.0}%",
1164 i18n.get_msg("common-stats-effect-power"),
1165 (stats.effect_power - 1.0) * 100.0
1166 )
1167 } else {
1168 format!(
1169 "{} : x{:.2}",
1170 i18n.get_msg("common-stats-effect-power"),
1171 stats.effect_power
1172 )
1173 };
1174 widget::Text::new(&effect_power_text)
1175 .graphics_for(id)
1176 .parent(id)
1177 .with_style(self.style.desc)
1178 .color(text_color)
1179 .down_from(state.ids.stats[1], V_PAD_STATS)
1180 .set(state.ids.stats[2], ui);
1181
1182 let range_text = if is_primary {
1184 format!(
1185 "{} : {:.0}%",
1186 i18n.get_msg("common-stats-range"),
1187 (stats.range - 1.0) * 100.0
1188 )
1189 } else {
1190 format!(
1191 "{} : x{:.2}",
1192 i18n.get_msg("common-stats-range"),
1193 stats.range
1194 )
1195 };
1196 widget::Text::new(&range_text)
1197 .graphics_for(id)
1198 .parent(id)
1199 .with_style(self.style.desc)
1200 .color(text_color)
1201 .down_from(state.ids.stats[2], V_PAD_STATS)
1202 .set(state.ids.stats[3], ui);
1203
1204 let energy_eff_text = if is_primary {
1206 format!(
1207 "{} : {:.0}%",
1208 i18n.get_msg("common-stats-energy_efficiency"),
1209 (stats.energy_efficiency - 1.0) * 100.0
1210 )
1211 } else {
1212 format!(
1213 "{} : x{:.2}",
1214 i18n.get_msg("common-stats-energy_efficiency"),
1215 stats.energy_efficiency
1216 )
1217 };
1218 widget::Text::new(&energy_eff_text)
1219 .graphics_for(id)
1220 .parent(id)
1221 .with_style(self.style.desc)
1222 .color(text_color)
1223 .down_from(state.ids.stats[3], V_PAD_STATS)
1224 .set(state.ids.stats[4], ui);
1225
1226 let buff_str_text = if is_primary {
1228 format!(
1229 "{} : {:.0}%",
1230 i18n.get_msg("common-stats-buff_strength"),
1231 (stats.buff_strength - 1.0) * 100.0
1232 )
1233 } else {
1234 format!(
1235 "{} : x{:.2}",
1236 i18n.get_msg("common-stats-buff_strength"),
1237 stats.buff_strength
1238 )
1239 };
1240 widget::Text::new(&buff_str_text)
1241 .graphics_for(id)
1242 .parent(id)
1243 .with_style(self.style.desc)
1244 .color(text_color)
1245 .down_from(state.ids.stats[4], V_PAD_STATS)
1246 .set(state.ids.stats[5], ui);
1247 }
1248 },
1249 _ => (),
1250 }
1251
1252 if !desc.is_empty() {
1254 widget::Text::new(&format!("\"{}\"", &desc))
1255 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1256 .graphics_for(id)
1257 .parent(id)
1258 .with_style(self.style.desc)
1259 .color(conrod_core::color::GREY)
1260 .down_from(
1261 if stats_count > 0 {
1262 state.ids.stats[state.ids.stats.len() - 1]
1263 } else {
1264 state.ids.item_frame
1265 },
1266 V_PAD,
1267 )
1268 .w(text_w)
1269 .set(state.ids.desc, ui);
1270 }
1271
1272 if let Some((buy, sell, factor)) =
1274 util::price_desc(self.prices, item.item_definition_id(), i18n)
1275 {
1276 widget::Text::new(&buy)
1277 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1278 .graphics_for(id)
1279 .parent(id)
1280 .with_style(self.style.desc)
1281 .color(Color::Rgba(factor, 1.0 - factor, 0.00, 1.0))
1282 .down_from(
1283 if !desc.is_empty() {
1284 state.ids.desc
1285 } else if stats_count > 0 {
1286 state.ids.stats[state.ids.stats.len() - 1]
1287 } else {
1288 state.ids.item_frame
1289 },
1290 V_PAD,
1291 )
1292 .w(text_w)
1293 .set(state.ids.prices_buy, ui);
1294
1295 widget::Text::new(&sell)
1296 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1297 .graphics_for(id)
1298 .parent(id)
1299 .with_style(self.style.desc)
1300 .color(Color::Rgba(1.0 - factor, factor, 0.00, 1.0))
1301 .down_from(state.ids.prices_buy, V_PAD_STATS)
1302 .w(text_w)
1303 .set(state.ids.prices_sell, ui);
1304
1305 widget::Text::new(&format!(
1307 "{}\n{}",
1308 i18n.get_msg("hud-trade-tooltip_hint_1"),
1309 i18n.get_msg("hud-trade-tooltip_hint_2"),
1310 ))
1311 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1312 .graphics_for(id)
1313 .parent(id)
1314 .with_style(self.style.desc)
1315 .color(Color::Rgba(255.0, 255.0, 255.0, 1.0))
1316 .down_from(state.ids.prices_sell, V_PAD_STATS)
1317 .w(text_w)
1318 .set(state.ids.tooltip_hints, ui);
1319 }
1320 }
1321
1322 fn default_x_dimension(&self, _ui: &Ui) -> Dimension { Dimension::Absolute(WIDTH) }
1325
1326 fn default_y_dimension(&self, ui: &Ui) -> Dimension {
1327 let item = &self.item;
1328
1329 let (_, desc) = util::item_text(item, self.localized_strings, self.item_i18n);
1330
1331 let (text_w, _image_w) = self.text_image_width(WIDTH);
1332
1333 let frame_h = ICON_SIZE[1] + V_PAD;
1335
1336 let stats_count = util::line_count(self.item, self.msm, self.localized_strings);
1338 let stat_h = if stats_count > 0 {
1339 widget::Text::new("placeholder")
1340 .with_style(self.style.desc)
1341 .get_h(ui)
1342 .unwrap_or(0.0)
1343 * stats_count as f64
1344 + (stats_count - 1) as f64 * V_PAD_STATS
1345 + V_PAD
1346 } else {
1347 0.0
1348 };
1349
1350 let desc_h: f64 = if !desc.is_empty() {
1352 widget::Text::new(&format!("\"{}\"", &desc))
1353 .with_style(self.style.desc)
1354 .w(text_w)
1355 .get_h(ui)
1356 .unwrap_or(0.0)
1357 + V_PAD
1358 } else {
1359 0.0
1360 };
1361
1362 let price_h: f64 = if let Some((buy, sell, _)) = util::price_desc(
1364 self.prices,
1365 item.item_definition_id(),
1366 self.localized_strings,
1367 ) {
1368 let tt_hint_1 = self.localized_strings.get_msg("hud-trade-tooltip_hint_1");
1371 let tt_hint_2 = self.localized_strings.get_msg("hud-trade-tooltip_hint_2");
1372
1373 widget::Text::new(&format!("{}\n{}\n{}\n{}", buy, sell, tt_hint_1, tt_hint_2))
1374 .with_style(self.style.desc)
1375 .w(text_w)
1376 .get_h(ui)
1377 .unwrap_or(0.0)
1378 + V_PAD * 2.0
1379 } else {
1380 0.0
1381 };
1382
1383 let height = frame_h + stat_h + desc_h + price_h + V_PAD + 5.0;
1385 Dimension::Absolute(height)
1386 }
1387}
1388
1389impl Colorable for ItemTooltip<'_> {
1390 builder_method!(color { style.color = Some(Color) });
1391}