Skip to main content

veloren_voxygen/hud/
loot_scroller.rs

1use super::{
2    HudInfo, Show, TEXT_COLOR, Windows, animate_by_pulse, get_quality_col,
3    img_ids::{Imgs, ImgsRot},
4    item_imgs::ItemImgs,
5    util,
6};
7use crate::ui::{ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, fonts::Fonts};
8use client::Client;
9use common::{
10    comp::{
11        FrontendItem, Inventory,
12        inventory::item::{ItemDesc, ItemI18n, MaterialStatManifest, Quality},
13    },
14    recipe::RecipeBookManifest,
15    uid::Uid,
16};
17use common_net::sync::WorldSyncExt;
18use conrod_core::{
19    Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
20    position::Dimension,
21    widget::{self, Image, List, Rectangle, Scrollbar, Text},
22    widget_ids,
23};
24use i18n::Localization;
25use std::{collections::VecDeque, num::NonZeroU32};
26
27widget_ids! {
28    struct Ids{
29        message_box,
30        scrollbar,
31        message_icons[],
32        message_icon_bgs[],
33        message_icon_frames[],
34        message_texts[],
35        message_text_shadows[],
36    }
37}
38
39const MAX_MESSAGES: usize = 50;
40
41const BOX_WIDTH: f64 = 300.0;
42const BOX_HEIGHT: f64 = 350.0;
43
44const ICON_BG_SIZE: f64 = 33.0;
45const ICON_SIZE: f64 = 30.0;
46const ICON_LABEL_SPACER: f64 = 7.0;
47
48const MESSAGE_VERTICAL_PADDING: f64 = 1.0;
49
50const HOVER_FADE_OUT_TIME: f32 = 2.0;
51const MESSAGE_FADE_OUT_TIME: f32 = 4.5;
52const AUTO_SHOW_FADE_OUT_TIME: f32 = 1.0;
53
54const MAX_MERGE_TIME: f32 = MESSAGE_FADE_OUT_TIME;
55
56#[derive(WidgetCommon)]
57pub struct LootScroller<'a> {
58    new_messages: &'a mut VecDeque<LootMessage>,
59
60    client: &'a Client,
61    info: &'a HudInfo<'a>,
62    show: &'a Show,
63    imgs: &'a Imgs,
64    item_imgs: &'a ItemImgs,
65    rot_imgs: &'a ImgsRot,
66    fonts: &'a Fonts,
67    localized_strings: &'a Localization,
68    item_i18n: &'a ItemI18n,
69    msm: &'a MaterialStatManifest,
70    rbm: &'a RecipeBookManifest,
71    inventory: Option<&'a Inventory>,
72    item_tooltip_manager: &'a mut ItemTooltipManager,
73    pulse: f32,
74
75    #[conrod(common_builder)]
76    common: widget::CommonBuilder,
77}
78impl<'a> LootScroller<'a> {
79    pub fn new(
80        new_messages: &'a mut VecDeque<LootMessage>,
81        client: &'a Client,
82        info: &'a HudInfo,
83        show: &'a Show,
84        imgs: &'a Imgs,
85        item_imgs: &'a ItemImgs,
86        rot_imgs: &'a ImgsRot,
87        fonts: &'a Fonts,
88        localized_strings: &'a Localization,
89        item_i18n: &'a ItemI18n,
90        msm: &'a MaterialStatManifest,
91        rbm: &'a RecipeBookManifest,
92        inventory: Option<&'a Inventory>,
93        item_tooltip_manager: &'a mut ItemTooltipManager,
94        pulse: f32,
95    ) -> Self {
96        Self {
97            new_messages,
98            client,
99            info,
100            show,
101            imgs,
102            item_imgs,
103            rot_imgs,
104            fonts,
105            localized_strings,
106            item_i18n,
107            msm,
108            rbm,
109            inventory,
110            item_tooltip_manager,
111            pulse,
112            common: widget::CommonBuilder::default(),
113        }
114    }
115}
116
117#[derive(Debug, PartialEq)]
118pub struct LootMessage {
119    pub item: FrontendItem,
120    pub amount: NonZeroU32,
121    pub taken_by: Uid,
122}
123
124pub struct State {
125    ids: Ids,
126    messages: VecDeque<(LootMessage, f32)>, // (message, timestamp)
127
128    last_hover_pulse: Option<f32>,
129    last_auto_show_pulse: Option<f32>, // auto show if (for example) bag is open
130}
131
132impl Widget for LootScroller<'_> {
133    type Event = ();
134    type State = State;
135    type Style = ();
136
137    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
138        State {
139            ids: Ids::new(id_gen),
140            messages: VecDeque::new(),
141            last_hover_pulse: None,
142            last_auto_show_pulse: None,
143        }
144    }
145
146    fn style(&self) -> Self::Style {}
147
148    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
149        let widget::UpdateArgs { state, ui, .. } = args;
150
151        // Tooltips
152        let item_tooltip = ItemTooltip::new(
153            {
154                // Edge images [t, b, r, l]
155                // Corner images [tr, tl, br, bl]
156                let edge = &self.rot_imgs.tt_side;
157                let corner = &self.rot_imgs.tt_corner;
158                ImageFrame::new(
159                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
160                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
161                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
162                    5.0,
163                )
164            },
165            self.client,
166            self.info,
167            self.imgs,
168            self.item_imgs,
169            self.pulse,
170            self.msm,
171            self.rbm,
172            self.inventory,
173            self.localized_strings,
174            self.item_i18n,
175        )
176        .title_font_size(self.fonts.cyri.scale(20))
177        .parent(ui.window)
178        .desc_font_size(self.fonts.cyri.scale(12))
179        .font_id(self.fonts.cyri.conrod_id)
180        .desc_text_color(TEXT_COLOR);
181
182        if !self.new_messages.is_empty() {
183            let pulse = self.pulse;
184            let oldest_merge_pulse = pulse - MAX_MERGE_TIME;
185
186            state.update(|s| {
187                s.messages.retain(|(message, t)| {
188                    if *t >= oldest_merge_pulse {
189                        if let Some(i) = self.new_messages.iter().position(|m| {
190                            m.item.item_definition_id() == message.item.item_definition_id()
191                                && m.taken_by == message.taken_by
192                        }) {
193                            self.new_messages[i].amount = self.new_messages[i]
194                                .amount
195                                .saturating_add(message.amount.get());
196                            false
197                        } else {
198                            true
199                        }
200                    } else {
201                        true
202                    }
203                });
204                s.messages
205                    .extend(self.new_messages.drain(..).map(|message| (message, pulse)));
206                while s.messages.len() > MAX_MESSAGES {
207                    s.messages.pop_front();
208                }
209            });
210            ui.scroll_widget(state.ids.message_box, [0.0, f64::MAX]);
211        }
212
213        // check if it collides with other windows
214        if self.show.diary
215            || self.show.map
216            || self.show.open_windows != Windows::None
217            || self.show.social
218            || self.show.trade
219        {
220            if state.last_hover_pulse.is_some() || state.last_auto_show_pulse.is_some() {
221                state.update(|s| {
222                    s.last_hover_pulse = None;
223                    s.last_auto_show_pulse = None;
224                });
225            }
226        } else {
227            //check if hovered
228            if ui
229                .rect_of(state.ids.message_box)
230                .map(|r| r.pad_left(-6.0))
231                .is_some_and(|r| r.is_over(ui.global_input().current.mouse.xy))
232            {
233                state.update(|s| s.last_hover_pulse = Some(self.pulse));
234            }
235
236            if state.ids.message_icons.len() < state.messages.len() {
237                state.update(|s| {
238                    s.ids
239                        .message_icons
240                        .resize(s.messages.len(), &mut ui.widget_id_generator())
241                });
242            }
243            if state.ids.message_icon_bgs.len() < state.messages.len() {
244                state.update(|s| {
245                    s.ids
246                        .message_icon_bgs
247                        .resize(s.messages.len(), &mut ui.widget_id_generator())
248                });
249            }
250            if state.ids.message_icon_frames.len() < state.messages.len() {
251                state.update(|s| {
252                    s.ids
253                        .message_icon_frames
254                        .resize(s.messages.len(), &mut ui.widget_id_generator())
255                });
256            }
257            if state.ids.message_texts.len() < state.messages.len() {
258                state.update(|s| {
259                    s.ids
260                        .message_texts
261                        .resize(s.messages.len(), &mut ui.widget_id_generator())
262                });
263            }
264            if state.ids.message_text_shadows.len() < state.messages.len() {
265                state.update(|s| {
266                    s.ids
267                        .message_text_shadows
268                        .resize(s.messages.len(), &mut ui.widget_id_generator())
269                });
270            }
271
272            let hover_age = state
273                .last_hover_pulse
274                .map_or(1.0, |t| (self.pulse - t) / HOVER_FADE_OUT_TIME);
275            let auto_show_age = state
276                .last_auto_show_pulse
277                .map_or(1.0, |t| (self.pulse - t) / AUTO_SHOW_FADE_OUT_TIME);
278
279            let show_all_age = hover_age.min(auto_show_age);
280
281            let messages_to_display = state
282                .messages
283                .iter()
284                .rev()
285                .map(|(message, t)| {
286                    let age = (self.pulse - t) / MESSAGE_FADE_OUT_TIME;
287                    (message, age)
288                })
289                .filter(|(_, age)| age.min(show_all_age) < 1.0)
290                .collect::<Vec<_>>();
291
292            let (mut list_messages, _) = List::flow_up(messages_to_display.len())
293                .w_h(BOX_WIDTH, BOX_HEIGHT)
294                .scroll_kids_vertically()
295                .bottom_left_with_margins_on(ui.window, 308.0, 20.0)
296                .set(state.ids.message_box, ui);
297
298            //only show scrollbar if it is being hovered and needed
299            if show_all_age < 1.0
300                && ui
301                    .widget_graph()
302                    .widget(state.ids.message_box)
303                    .and_then(|w| w.maybe_y_scroll_state)
304                    .is_some_and(|s| s.scrollable_range_len > BOX_HEIGHT)
305            {
306                Scrollbar::y_axis(state.ids.message_box)
307                    .thickness(5.0)
308                    .rgba(0.33, 0.33, 0.33, 1.0 - show_all_age.powi(4))
309                    .left_from(state.ids.message_box, 1.0)
310                    .set(state.ids.scrollbar, ui);
311            }
312
313            let stats = self.client.state().read_storage::<common::comp::Stats>();
314
315            while let Some(list_message) = list_messages.next(ui) {
316                let i = list_message.i;
317
318                let (message, age) = messages_to_display[i];
319                let LootMessage {
320                    item,
321                    amount,
322                    taken_by,
323                } = message;
324
325                let alpha = 1.0 - age.min(show_all_age).powi(4);
326
327                let brightness = 1.0 / (age / 0.05 - 1.0).abs().clamp(0.01, 1.0);
328
329                let shade_color = |color: Color| {
330                    let color::Hsla(hue, sat, lum, alp) = color.to_hsl();
331                    color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha)
332                };
333
334                let quality_col_image = match item.quality() {
335                    Quality::Low => self.imgs.inv_slot_grey,
336                    Quality::Common => self.imgs.inv_slot_common,
337                    Quality::Moderate => self.imgs.inv_slot_green,
338                    Quality::High => self.imgs.inv_slot_blue,
339                    Quality::Epic => self.imgs.inv_slot_purple,
340                    Quality::Legendary => self.imgs.inv_slot_gold,
341                    Quality::Artifact => self.imgs.inv_slot_orange,
342                    _ => self.imgs.inv_slot_red,
343                };
344                let quality_col = get_quality_col(item.quality());
345
346                Image::new(self.imgs.pixel)
347                    .color(Some(shade_color(quality_col.alpha(0.7))))
348                    .w_h(ICON_BG_SIZE, ICON_BG_SIZE)
349                    .top_left_with_margins_on(list_message.widget_id, MESSAGE_VERTICAL_PADDING, 0.0)
350                    .set(state.ids.message_icon_bgs[i], ui);
351
352                Image::new(quality_col_image)
353                    .color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0))))
354                    .wh_of(state.ids.message_icon_bgs[i])
355                    .middle_of(state.ids.message_icon_bgs[i])
356                    .set(state.ids.message_icon_frames[i], ui);
357
358                Image::new(animate_by_pulse(
359                    &self.item_imgs.img_ids_or_not_found_img(item.into()),
360                    self.pulse,
361                ))
362                .color(Some(shade_color(color::hsla(0.0, 0.0, 1.0, 1.0))))
363                .w_h(ICON_SIZE, ICON_SIZE)
364                .middle_of(state.ids.message_icon_bgs[i])
365                .with_item_tooltip(
366                    self.item_tooltip_manager,
367                    core::iter::once(item as &dyn ItemDesc),
368                    &None,
369                    &item_tooltip,
370                )
371                .set(state.ids.message_icons[i], ui);
372
373                let target_name = self
374                    .client
375                    .player_list()
376                    .get(taken_by)
377                    .map_or_else(
378                        || {
379                            self.client
380                                .state()
381                                .ecs()
382                                .entity_from_uid(*taken_by)
383                                .and_then(|entity| {
384                                    stats
385                                        .get(entity)
386                                        .map(|e| self.localized_strings.get_content(&e.name))
387                                })
388                        },
389                        |info| Some(info.player_alias.clone()),
390                    )
391                    .unwrap_or_else(|| format!("<uid {}>", *taken_by));
392
393                let (user_gender, is_you) = match self.client.player_list().get(taken_by) {
394                    Some(player_info) => match player_info.character.as_ref() {
395                        Some(character_info) => (
396                            match character_info.gender {
397                                Some(common::comp::Gender::Feminine) => "she".to_string(),
398                                Some(common::comp::Gender::Masculine) => "he".to_string(),
399                                // TODO: players can't be neuter, for now
400                                Some(common::comp::Gender::Neuter) | None => "??".to_string(),
401                            },
402                            self.client.uid().expect("Client doesn't have a Uid!!!") == *taken_by,
403                        ),
404                        None => ("??".to_string(), false),
405                    },
406                    None => ("??".to_string(), false),
407                };
408
409                let label = if is_you {
410                    self.localized_strings.get_msg_ctx(
411                            "hud-loot-pickup-msg-you",
412                            &i18n::fluent_args! {
413                                  "gender" => user_gender,
414                                  "amount" => amount.get(),
415                                  "item" => {
416                                      let (name, _) =
417                                          util::item_text(&item, self.localized_strings, self.item_i18n);
418                                      name
419                                  },
420                            },
421                        )
422                } else {
423                    self.localized_strings.get_msg_ctx(
424                            "hud-loot-pickup-msg",
425                            &i18n::fluent_args! {
426                                  "gender" => user_gender,
427                                  "actor" => &target_name,
428                                  "amount" => amount.get(),
429                                  "item" => {
430                                      let (name, _) =
431                                          util::item_text(&item, self.localized_strings, self.item_i18n);
432                                      name
433                                  },
434                            },
435                        )
436                };
437
438                let label_font_size = 20;
439
440                Text::new(&label)
441                    .top_left_with_margins_on(
442                        list_message.widget_id,
443                        MESSAGE_VERTICAL_PADDING + 1.0,
444                        ICON_BG_SIZE + ICON_LABEL_SPACER,
445                    )
446                    .font_id(self.fonts.cyri.conrod_id)
447                    .font_size(self.fonts.cyri.scale(label_font_size))
448                    .color(shade_color(quality_col))
449                    .graphics_for(state.ids.message_icons[i])
450                    .and(|text| {
451                        let text_width = match text.get_x_dimension(ui) {
452                            Dimension::Absolute(x) => x,
453                            _ => f64::MAX,
454                        }
455                        .min(BOX_WIDTH - (ICON_BG_SIZE + ICON_LABEL_SPACER));
456                        text.w(text_width)
457                    })
458                    .set(state.ids.message_texts[i], ui);
459                Text::new(&label)
460                    .depth(1.0)
461                    .parent(list_message.widget_id)
462                    .x_y_relative_to(state.ids.message_texts[i], -1.0, -1.0)
463                    .wh_of(state.ids.message_texts[i])
464                    .font_id(self.fonts.cyri.conrod_id)
465                    .font_size(self.fonts.cyri.scale(label_font_size))
466                    .color(shade_color(color::rgba(0.0, 0.0, 0.0, 1.0)))
467                    .set(state.ids.message_text_shadows[i], ui);
468
469                let height = 2.0 * MESSAGE_VERTICAL_PADDING
470                    + ICON_BG_SIZE.max(
471                        1.0 + ui
472                            .rect_of(state.ids.message_texts[i])
473                            .map_or(0.0, |r| r.h() + label_font_size as f64 / 3.0),
474                        /* add to height since rect height does not account for lower parts of
475                         * letters */
476                    );
477
478                let rect = Rectangle::fill_with([BOX_WIDTH, height], color::TRANSPARENT);
479
480                list_message.set(rect, ui);
481            }
482        }
483    }
484}