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 if start.elapsed() > self.fade_dur {
106 self.state = match maybe_hover {
107 Some((start, hover)) => HoverState::Start(start, hover),
108 None => HoverState::None,
109 };
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 if let ItemKind::Tool(equipped_tool) = &*equipped_item.kind() {
716 let tool_stats = tool.stats(item.stats_durability_multiplier());
717 let equipped_tool_stats =
718 equipped_tool.stats(equipped_item.stats_durability_multiplier());
719 let diff = tool_stats - equipped_tool_stats;
720 let power_diff =
721 util::comparison(tool_stats.power, equipped_tool_stats.power);
722 let speed_diff =
723 util::comparison(tool_stats.speed, equipped_tool_stats.speed);
724 let effect_power_diff = util::comparison(
725 tool_stats.effect_power,
726 equipped_tool_stats.effect_power,
727 );
728 let range_diff =
729 util::comparison(tool_stats.range, equipped_tool_stats.range);
730 let energy_efficiency_diff = util::comparison(
731 tool_stats.energy_efficiency,
732 equipped_tool_stats.energy_efficiency,
733 );
734 let buff_strength_diff = util::comparison(
735 tool_stats.buff_strength,
736 equipped_tool_stats.buff_strength,
737 );
738
739 let tool_durability =
740 util::item_durability(item).unwrap_or(Item::MAX_DURABILITY);
741 let equipped_durability =
742 util::item_durability(equipped_item).unwrap_or(Item::MAX_DURABILITY);
743 let durability_diff =
744 util::comparison(tool_durability, equipped_durability);
745
746 let mut diff_text = |text: String, color, id_index| {
747 widget::Text::new(&text)
748 .align_middle_y_of(state.ids.stats[id_index])
749 .right_from(state.ids.stats[id_index], H_PAD)
750 .graphics_for(id)
751 .parent(id)
752 .with_style(style)
753 .color(color)
754 .set(state.ids.diffs[id_index], ui)
755 };
756
757 if diff.power.abs() > f32::EPSILON {
758 let text = format!("{} {:.1}", &power_diff.0, &diff.power * 10.0);
759 diff_text(text, power_diff.1, 0)
760 }
761 if diff.speed.abs() > f32::EPSILON {
762 let text = format!("{} {:+.0}", &speed_diff.0, &diff.speed * 100.0);
763 diff_text(text, speed_diff.1, 1)
764 }
765 if diff.effect_power.abs() > f32::EPSILON {
766 let text = format!(
767 "{} {:+.0}",
768 &effect_power_diff.0,
769 &diff.effect_power * 100.0
770 );
771 diff_text(text, effect_power_diff.1, 2)
772 }
773 if diff.range.abs() > f32::EPSILON {
774 let text = format!("{} {:.1}%", &range_diff.0, &diff.range * 100.0);
775 diff_text(text, range_diff.1, 3)
776 }
777 if diff.energy_efficiency.abs() > f32::EPSILON {
778 let text = format!(
779 "{} {:.1}%",
780 &energy_efficiency_diff.0,
781 &diff.energy_efficiency * 100.0
782 );
783 diff_text(text, energy_efficiency_diff.1, 4)
784 }
785 if diff.buff_strength.abs() > f32::EPSILON {
786 let text = format!(
787 "{} {:.1}%",
788 &buff_strength_diff.0,
789 &diff.buff_strength * 100.0
790 );
791 diff_text(text, buff_strength_diff.1, 5)
792 }
793 if tool_durability != equipped_durability && item.has_durability() {
794 let text = format!(
795 "{} {}",
796 &durability_diff.0,
797 tool_durability as i32 - equipped_durability as i32
798 );
799 diff_text(text, durability_diff.1, 6)
800 }
801 }
802 }
803 },
804 ItemKind::Armor(armor) => {
805 let armor_stats = armor.stats(self.msm, item.stats_durability_multiplier());
806
807 let mut stat_text = |text: String, i: usize| {
808 widget::Text::new(&text)
809 .graphics_for(id)
810 .parent(id)
811 .with_style(self.style.desc)
812 .color(text_color)
813 .and(|t| {
814 if i == 0 {
815 t.x_align_to(
816 state.ids.item_frame,
817 conrod_core::position::Align::Start,
818 )
819 .down_from(state.ids.item_frame, V_PAD)
820 } else {
821 t.down_from(state.ids.stats[i - 1], V_PAD_STATS)
822 }
823 })
824 .set(state.ids.stats[i], ui);
825 };
826
827 let mut index = 0;
828
829 if armor_stats.protection.is_some() {
830 stat_text(
831 format!(
832 "{} : {}",
833 i18n.get_msg("common-stats-armor"),
834 util::protec2string(
835 armor_stats.protection.unwrap_or(Protection::Normal(0.0))
836 )
837 ),
838 index,
839 );
840 index += 1;
841 }
842
843 if armor_stats.poise_resilience.is_some() {
845 stat_text(
846 format!(
847 "{} : {}",
848 i18n.get_msg("common-stats-poise_res"),
849 util::protec2string(
850 armor_stats
851 .poise_resilience
852 .unwrap_or(Protection::Normal(0.0))
853 )
854 ),
855 index,
856 );
857 index += 1;
858 }
859
860 if armor_stats.energy_max.is_some() {
862 stat_text(
863 format!(
864 "{} : {:.1}",
865 i18n.get_msg("common-stats-energy_max"),
866 armor_stats.energy_max.unwrap_or(0.0)
867 ),
868 index,
869 );
870 index += 1;
871 }
872
873 if armor_stats.energy_reward.is_some() {
875 stat_text(
876 format!(
877 "{} : {:.1}%",
878 i18n.get_msg("common-stats-energy_reward"),
879 armor_stats.energy_reward.map_or(0.0, |x| x * 100.0)
880 ),
881 index,
882 );
883 index += 1;
884 }
885
886 if armor_stats.precision_power.is_some() {
888 stat_text(
889 format!(
890 "{} : {:.3}",
891 i18n.get_msg("common-stats-precision_power"),
892 armor_stats.precision_power.unwrap_or(0.0)
893 ),
894 index,
895 );
896 index += 1;
897 }
898
899 if armor_stats.stealth.is_some() {
901 stat_text(
902 format!(
903 "{} : {:.3}",
904 i18n.get_msg("common-stats-stealth"),
905 armor_stats.stealth.unwrap_or(0.0)
906 ),
907 index,
908 );
909 index += 1;
910 }
911
912 if item.num_slots() > 0 {
914 stat_text(
915 format!(
916 "{} : {}",
917 i18n.get_msg("common-stats-slots"),
918 item.num_slots()
919 ),
920 index,
921 );
922 index += 1;
923 }
924
925 if let Some(durability) = util::item_durability(item) {
926 stat_text(
927 format!(
928 "{} : {}/{}",
929 i18n.get_msg("common-stats-durability"),
930 durability,
931 Item::MAX_DURABILITY
932 ),
933 index,
934 );
935 }
936
937 if let Some(equipped_item) = equipped_item {
938 if let ItemKind::Armor(equipped_armor) = &*equipped_item.kind() {
939 let equipped_stats = equipped_armor
940 .stats(self.msm, equipped_item.stats_durability_multiplier());
941 let diff = armor_stats - equipped_stats;
942 let protection_diff = util::option_comparison(
943 &armor_stats.protection,
944 &equipped_stats.protection,
945 );
946 let poise_res_diff = util::option_comparison(
947 &armor_stats.poise_resilience,
948 &equipped_stats.poise_resilience,
949 );
950 let energy_max_diff = util::option_comparison(
951 &armor_stats.energy_max,
952 &equipped_stats.energy_max,
953 );
954 let energy_reward_diff = util::option_comparison(
955 &armor_stats.energy_reward,
956 &equipped_stats.energy_reward,
957 );
958 let precision_power_diff = util::option_comparison(
959 &armor_stats.precision_power,
960 &equipped_stats.precision_power,
961 );
962 let stealth_diff =
963 util::option_comparison(&armor_stats.stealth, &equipped_stats.stealth);
964
965 let armor_durability = util::item_durability(item);
966 let equipped_durability = util::item_durability(equipped_item);
967 let durability_diff =
968 util::option_comparison(&armor_durability, &equipped_durability);
969
970 let mut diff_text = |text: String, color, id_index| {
971 widget::Text::new(&text)
972 .align_middle_y_of(state.ids.stats[id_index])
973 .right_from(state.ids.stats[id_index], H_PAD)
974 .graphics_for(id)
975 .parent(id)
976 .with_style(style)
977 .color(color)
978 .set(state.ids.diffs[id_index], ui)
979 };
980
981 let mut index = 0;
982 if let Some(p_diff) = diff.protection {
983 if p_diff != Protection::Normal(0.0) {
984 let text = format!(
985 "{} {}",
986 &protection_diff.0,
987 util::protec2string(p_diff)
988 );
989 diff_text(text, protection_diff.1, index);
990 }
991 }
992 index += armor_stats.protection.is_some() as usize;
993
994 if let Some(p_r_diff) = diff.poise_resilience {
995 if p_r_diff != Protection::Normal(0.0) {
996 let text = format!(
997 "{} {}",
998 &poise_res_diff.0,
999 util::protec2string(p_r_diff)
1000 );
1001 diff_text(text, poise_res_diff.1, index);
1002 }
1003 }
1004 index += armor_stats.poise_resilience.is_some() as usize;
1005
1006 if let Some(e_m_diff) = diff.energy_max {
1007 if e_m_diff.abs() > Energy::ENERGY_EPSILON {
1008 let text = format!("{} {:.1}", &energy_max_diff.0, e_m_diff);
1009 diff_text(text, energy_max_diff.1, index);
1010 }
1011 }
1012 index += armor_stats.energy_max.is_some() as usize;
1013
1014 if let Some(e_r_diff) = diff.energy_reward {
1015 if e_r_diff.abs() > Energy::ENERGY_EPSILON {
1016 let text =
1017 format!("{} {:.1}", &energy_reward_diff.0, e_r_diff * 100.0);
1018 diff_text(text, energy_reward_diff.1, index);
1019 }
1020 }
1021 index += armor_stats.energy_reward.is_some() as usize;
1022
1023 if let Some(p_p_diff) = diff.precision_power {
1024 if p_p_diff != 0.0_f32 {
1025 let text = format!("{} {:.3}", &precision_power_diff.0, p_p_diff);
1026 diff_text(text, precision_power_diff.1, index);
1027 }
1028 }
1029 index += armor_stats.precision_power.is_some() as usize;
1030
1031 if let Some(s_diff) = diff.stealth {
1032 if s_diff != 0.0_f32 {
1033 let text = format!("{} {:.3}", &stealth_diff.0, s_diff);
1034 diff_text(text, stealth_diff.1, index);
1035 }
1036 }
1037 index += armor_stats.stealth.is_some() as usize;
1038
1039 if armor_durability != equipped_durability && item.has_durability() {
1040 let diff = armor_durability.unwrap_or(Item::MAX_DURABILITY) as i32
1041 - equipped_durability.unwrap_or(Item::MAX_DURABILITY) as i32;
1042 let text = format!("{} {}", &durability_diff.0, diff);
1043 diff_text(text, durability_diff.1, index);
1044 }
1045 }
1046 }
1047 },
1048 ItemKind::Consumable { effects, .. } => {
1049 for (i, desc) in util::consumable_desc(effects, i18n).iter().enumerate() {
1050 let (down_from, pad) = match i {
1051 0 => (state.ids.item_frame, V_PAD),
1052 _ => (state.ids.stats[i - 1], V_PAD_STATS),
1053 };
1054 widget::Text::new(desc)
1055 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1056 .graphics_for(id)
1057 .parent(id)
1058 .with_style(self.style.desc)
1059 .color(text_color)
1060 .down_from(down_from, pad)
1061 .set(state.ids.stats[i], ui);
1062 }
1063 },
1064 ItemKind::RecipeGroup { recipes } => {
1065 const KNOWN_COLOR: Color = Color::Rgba(0.0, 1.0, 0.0, 1.0);
1066 const NOT_KNOWN_COLOR: Color = Color::Rgba(1.0, 0.0, 0.0, 1.0);
1067 let recipe_known = self.inventory.map(|inv| {
1068 inv.recipe_groups_iter()
1069 .any(|group| group.item_definition_id() == item.item_definition_id())
1070 });
1071 let known_key_color = match recipe_known {
1072 Some(false) => Some(("items-common-recipe-not_known", NOT_KNOWN_COLOR)),
1073 Some(true) => Some(("items-common-recipe-known", KNOWN_COLOR)),
1074 None => None,
1075 };
1076 if let Some((recipe_known_key, recipe_known_color)) = known_key_color {
1077 let recipe_known_text = self.localized_strings.get_msg(recipe_known_key);
1078 widget::Text::new(&recipe_known_text)
1079 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1080 .graphics_for(id)
1081 .parent(id)
1082 .with_style(self.style.desc)
1083 .color(recipe_known_color)
1084 .down_from(state.ids.item_frame, V_PAD)
1085 .set(state.ids.stats[0], ui);
1086 }
1087 let offset = if known_key_color.is_some() { 1 } else { 0 };
1088 for (i, desc) in recipes.iter().enumerate().map(|(i, s)| (i + offset, s)) {
1089 if let Some(recipe) = self.rbm.get(desc) {
1090 let (item_name, _) = util::item_text(
1091 recipe.output.0.as_ref(),
1092 self.localized_strings,
1093 self.item_i18n,
1094 );
1095 let (down_from, pad) = match i {
1096 0 => (state.ids.item_frame, V_PAD),
1097 _ => (state.ids.stats[i - 1], V_PAD_STATS),
1098 };
1099 widget::Text::new(&item_name)
1100 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1101 .graphics_for(id)
1102 .parent(id)
1103 .with_style(self.style.desc)
1104 .color(text_color)
1105 .down_from(down_from, pad)
1106 .set(state.ids.stats[i], ui);
1107 }
1108 }
1109 },
1110 ItemKind::ModularComponent(mc) => {
1111 if let Some(stats) = mc.tool_stats(item.components(), self.msm) {
1112 let is_primary = matches!(mc, ModularComponent::ToolPrimaryComponent { .. });
1113
1114 let power_text = if is_primary {
1116 format!(
1117 "{} : {:.1}",
1118 i18n.get_msg("common-stats-power"),
1119 stats.power * 10.0
1120 )
1121 } else {
1122 format!(
1123 "{} : x{:.2}",
1124 i18n.get_msg("common-stats-power"),
1125 stats.power
1126 )
1127 };
1128 widget::Text::new(&power_text)
1129 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1130 .graphics_for(id)
1131 .parent(id)
1132 .with_style(self.style.desc)
1133 .color(text_color)
1134 .down_from(state.ids.item_frame, V_PAD)
1135 .set(state.ids.stats[0], ui);
1136
1137 let speed_text = if is_primary {
1139 format!(
1140 "{} : {:+.0}%",
1141 i18n.get_msg("common-stats-speed"),
1142 (stats.speed - 1.0) * 100.0
1143 )
1144 } else {
1145 format!(
1146 "{} : x{:.2}",
1147 i18n.get_msg("common-stats-speed"),
1148 stats.speed
1149 )
1150 };
1151 widget::Text::new(&speed_text)
1152 .graphics_for(id)
1153 .parent(id)
1154 .with_style(self.style.desc)
1155 .color(text_color)
1156 .down_from(state.ids.stats[0], V_PAD_STATS)
1157 .set(state.ids.stats[1], ui);
1158
1159 let effect_power_text = if is_primary {
1163 format!(
1164 "{} : {:+.0}%",
1165 i18n.get_msg("common-stats-effect-power"),
1166 (stats.effect_power - 1.0) * 100.0
1167 )
1168 } else {
1169 format!(
1170 "{} : x{:.2}",
1171 i18n.get_msg("common-stats-effect-power"),
1172 stats.effect_power
1173 )
1174 };
1175 widget::Text::new(&effect_power_text)
1176 .graphics_for(id)
1177 .parent(id)
1178 .with_style(self.style.desc)
1179 .color(text_color)
1180 .down_from(state.ids.stats[1], V_PAD_STATS)
1181 .set(state.ids.stats[2], ui);
1182
1183 let range_text = if is_primary {
1185 format!(
1186 "{} : {:.0}%",
1187 i18n.get_msg("common-stats-range"),
1188 (stats.range - 1.0) * 100.0
1189 )
1190 } else {
1191 format!(
1192 "{} : x{:.2}",
1193 i18n.get_msg("common-stats-range"),
1194 stats.range
1195 )
1196 };
1197 widget::Text::new(&range_text)
1198 .graphics_for(id)
1199 .parent(id)
1200 .with_style(self.style.desc)
1201 .color(text_color)
1202 .down_from(state.ids.stats[2], V_PAD_STATS)
1203 .set(state.ids.stats[3], ui);
1204
1205 let energy_eff_text = if is_primary {
1207 format!(
1208 "{} : {:.0}%",
1209 i18n.get_msg("common-stats-energy_efficiency"),
1210 (stats.energy_efficiency - 1.0) * 100.0
1211 )
1212 } else {
1213 format!(
1214 "{} : x{:.2}",
1215 i18n.get_msg("common-stats-energy_efficiency"),
1216 stats.energy_efficiency
1217 )
1218 };
1219 widget::Text::new(&energy_eff_text)
1220 .graphics_for(id)
1221 .parent(id)
1222 .with_style(self.style.desc)
1223 .color(text_color)
1224 .down_from(state.ids.stats[3], V_PAD_STATS)
1225 .set(state.ids.stats[4], ui);
1226
1227 let buff_str_text = if is_primary {
1229 format!(
1230 "{} : {:.0}%",
1231 i18n.get_msg("common-stats-buff_strength"),
1232 (stats.buff_strength - 1.0) * 100.0
1233 )
1234 } else {
1235 format!(
1236 "{} : x{:.2}",
1237 i18n.get_msg("common-stats-buff_strength"),
1238 stats.buff_strength
1239 )
1240 };
1241 widget::Text::new(&buff_str_text)
1242 .graphics_for(id)
1243 .parent(id)
1244 .with_style(self.style.desc)
1245 .color(text_color)
1246 .down_from(state.ids.stats[4], V_PAD_STATS)
1247 .set(state.ids.stats[5], ui);
1248 }
1249 },
1250 _ => (),
1251 }
1252
1253 if !desc.is_empty() {
1255 widget::Text::new(&format!("\"{}\"", &desc))
1256 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1257 .graphics_for(id)
1258 .parent(id)
1259 .with_style(self.style.desc)
1260 .color(conrod_core::color::GREY)
1261 .down_from(
1262 if stats_count > 0 {
1263 state.ids.stats[state.ids.stats.len() - 1]
1264 } else {
1265 state.ids.item_frame
1266 },
1267 V_PAD,
1268 )
1269 .w(text_w)
1270 .set(state.ids.desc, ui);
1271 }
1272
1273 if let Some((buy, sell, factor)) =
1275 util::price_desc(self.prices, item.item_definition_id(), i18n)
1276 {
1277 widget::Text::new(&buy)
1278 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1279 .graphics_for(id)
1280 .parent(id)
1281 .with_style(self.style.desc)
1282 .color(Color::Rgba(factor, 1.0 - factor, 0.00, 1.0))
1283 .down_from(
1284 if !desc.is_empty() {
1285 state.ids.desc
1286 } else if stats_count > 0 {
1287 state.ids.stats[state.ids.stats.len() - 1]
1288 } else {
1289 state.ids.item_frame
1290 },
1291 V_PAD,
1292 )
1293 .w(text_w)
1294 .set(state.ids.prices_buy, ui);
1295
1296 widget::Text::new(&sell)
1297 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1298 .graphics_for(id)
1299 .parent(id)
1300 .with_style(self.style.desc)
1301 .color(Color::Rgba(1.0 - factor, factor, 0.00, 1.0))
1302 .down_from(state.ids.prices_buy, V_PAD_STATS)
1303 .w(text_w)
1304 .set(state.ids.prices_sell, ui);
1305
1306 widget::Text::new(&format!(
1308 "{}\n{}",
1309 i18n.get_msg("hud-trade-tooltip_hint_1"),
1310 i18n.get_msg("hud-trade-tooltip_hint_2"),
1311 ))
1312 .x_align_to(state.ids.item_frame, conrod_core::position::Align::Start)
1313 .graphics_for(id)
1314 .parent(id)
1315 .with_style(self.style.desc)
1316 .color(Color::Rgba(255.0, 255.0, 255.0, 1.0))
1317 .down_from(state.ids.prices_sell, V_PAD_STATS)
1318 .w(text_w)
1319 .set(state.ids.tooltip_hints, ui);
1320 }
1321 }
1322
1323 fn default_x_dimension(&self, _ui: &Ui) -> Dimension { Dimension::Absolute(WIDTH) }
1326
1327 fn default_y_dimension(&self, ui: &Ui) -> Dimension {
1328 let item = &self.item;
1329
1330 let (_, desc) = util::item_text(item, self.localized_strings, self.item_i18n);
1331
1332 let (text_w, _image_w) = self.text_image_width(WIDTH);
1333
1334 let frame_h = ICON_SIZE[1] + V_PAD;
1336
1337 let stats_count = util::line_count(self.item, self.msm, self.localized_strings);
1339 let stat_h = if stats_count > 0 {
1340 widget::Text::new("placeholder")
1341 .with_style(self.style.desc)
1342 .get_h(ui)
1343 .unwrap_or(0.0)
1344 * stats_count as f64
1345 + (stats_count - 1) as f64 * V_PAD_STATS
1346 + V_PAD
1347 } else {
1348 0.0
1349 };
1350
1351 let desc_h: f64 = if !desc.is_empty() {
1353 widget::Text::new(&format!("\"{}\"", &desc))
1354 .with_style(self.style.desc)
1355 .w(text_w)
1356 .get_h(ui)
1357 .unwrap_or(0.0)
1358 + V_PAD
1359 } else {
1360 0.0
1361 };
1362
1363 let price_h: f64 = if let Some((buy, sell, _)) = util::price_desc(
1365 self.prices,
1366 item.item_definition_id(),
1367 self.localized_strings,
1368 ) {
1369 let tt_hint_1 = self.localized_strings.get_msg("hud-trade-tooltip_hint_1");
1372 let tt_hint_2 = self.localized_strings.get_msg("hud-trade-tooltip_hint_2");
1373
1374 widget::Text::new(&format!("{}\n{}\n{}\n{}", buy, sell, tt_hint_1, tt_hint_2))
1375 .with_style(self.style.desc)
1376 .w(text_w)
1377 .get_h(ui)
1378 .unwrap_or(0.0)
1379 + V_PAD * 2.0
1380 } else {
1381 0.0
1382 };
1383
1384 let height = frame_h + stat_h + desc_h + price_h + V_PAD + 5.0;
1386 Dimension::Absolute(height)
1387 }
1388}
1389
1390impl Colorable for ItemTooltip<'_> {
1391 builder_method!(color { style.color = Some(Color) });
1392}