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