Skip to main content

veloren_voxygen/hud/
overitem.rs

1use super::img_ids::Imgs;
2
3use crate::{
4    GlobalState,
5    game_input::GameInput,
6    hud::{
7        CollectFailedData, HudCollectFailedReason, HudLootOwner, controller_icons as icon_utils,
8    },
9    ui::{RichText, fonts::Fonts},
10    window::LastInput,
11};
12use conrod_core::{
13    Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
14    widget::{self, RoundedRectangle, Text},
15    widget_ids,
16};
17use i18n::Localization;
18use std::borrow::Cow;
19
20pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0);
21pub const NEGATIVE_TEXT_COLOR: Color = Color::Rgba(0.91, 0.15, 0.17, 1.0);
22pub const PICKUP_FAILED_FADE_OUT_TIME: f32 = 1.5;
23
24widget_ids! {
25    struct Ids {
26        // Name
27        name_bg,
28        name,
29        // Interaction hints
30        btn_bg,
31        btns[],
32        // Inventory full
33        inv_full_bg,
34        inv_full,
35    }
36}
37
38/// UI widget containing everything that goes over a item
39/// (Item, DistanceFromPlayer, Rarity, etc.)
40#[derive(WidgetCommon)]
41pub struct Overitem<'a> {
42    name: Cow<'a, str>,
43    quality: Color,
44    distance_from_player_sqr: f32,
45    fonts: &'a Fonts,
46    localized_strings: &'a Localization,
47    #[conrod(common_builder)]
48    common: widget::CommonBuilder,
49    properties: OveritemProperties,
50    pulse: f32,
51    // GameInput optional so we can just show stuff like "needs pickaxe"
52    interaction_options: Vec<(Option<GameInput>, String, Color)>,
53    imgs: &'a Imgs,
54    global_state: &'a GlobalState,
55}
56
57impl<'a> Overitem<'a> {
58    pub fn new(
59        name: Cow<'a, str>,
60        quality: Color,
61        distance_from_player_sqr: f32,
62        fonts: &'a Fonts,
63        localized_strings: &'a Localization,
64        properties: OveritemProperties,
65        pulse: f32,
66        interaction_options: Vec<(Option<GameInput>, String, Color)>,
67        imgs: &'a Imgs,
68        global_state: &'a GlobalState,
69    ) -> Self {
70        Self {
71            name,
72            quality,
73            distance_from_player_sqr,
74            fonts,
75            localized_strings,
76            common: widget::CommonBuilder::default(),
77            properties,
78            pulse,
79            interaction_options,
80            imgs,
81            global_state,
82        }
83    }
84}
85
86pub struct OveritemProperties {
87    pub active: bool,
88    pub pickup_failed_pulse: Option<CollectFailedData>,
89}
90
91pub struct State {
92    ids: Ids,
93}
94
95impl Widget for Overitem<'_> {
96    type Event = ();
97    type State = State;
98    type Style = ();
99
100    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
101        State {
102            ids: Ids::new(id_gen),
103        }
104    }
105
106    fn style(&self) -> Self::Style {}
107
108    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
109        let widget::UpdateArgs { id, state, ui, .. } = args;
110
111        let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
112
113        // Example:
114        //            MUSHROOM
115        //              ___
116        //             | E |
117        //              ———
118
119        // Scale at max distance is 10, and at min distance is 30. Disabled since the
120        // scaling ruins glyph caching, causing performance issues near lootbags
121        // let scale: f64 = ((1.5
122        //     - (self.distance_from_player_sqr /
123        //       common::consts::MAX_PICKUP_RANGE.powi(2)))
124        //     * 20.0)
125        //     .into();
126        let scale = 30.0;
127
128        let text_font_size = scale * 1.0;
129        let text_pos_y = scale * 1.2;
130
131        let btn_rect_size = scale * 0.8;
132        let btn_font_size = scale * 0.6;
133        let btn_rect_pos_y = 0.0;
134        let btn_text_pos_y = btn_rect_pos_y + ((btn_rect_size - btn_font_size) * 0.5);
135        let btn_radius = btn_rect_size / 5.0;
136
137        let inv_full_font_size = scale * 1.0;
138        let inv_full_pos_y = scale * 2.4;
139
140        // Item Name
141        Text::new(&self.name)
142            .font_id(self.fonts.cyri.conrod_id)
143            .font_size(text_font_size as u32)
144            .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
145            .x_y(-1.0, text_pos_y - 2.0)
146            .parent(id)
147            .depth(self.distance_from_player_sqr + 4.0)
148            .set(state.ids.name_bg, ui);
149        Text::new(&self.name)
150            .font_id(self.fonts.cyri.conrod_id)
151            .font_size(text_font_size as u32)
152            .color(self.quality)
153            .x_y(0.0, text_pos_y)
154            .depth(self.distance_from_player_sqr + 3.0)
155            .parent(id)
156            .set(state.ids.name, ui);
157
158        // Interaction hints
159        if !self.interaction_options.is_empty() && self.properties.active {
160            let mut max_w = btn_rect_size;
161            let mut max_h = 0.0;
162
163            // get the display text based on input method
164            let texts: Vec<(String, &Color)> = match self.global_state.window.last_input() {
165                LastInput::KeyboardMouse => self
166                    .interaction_options
167                    .iter()
168                    .map(|(input, action, color)| {
169                        let output = if let Some(input) = input {
170                            match self.global_state.settings.controls.get_binding(*input) {
171                                Some(binding) => format!("{} {action}", binding.display_string()),
172                                None => format!("{} {action}", icon_utils::UNBOUND_KEY),
173                            }
174                        } else {
175                            action.to_string()
176                        };
177                        (output, color)
178                    })
179                    .collect(),
180                LastInput::Controller => self
181                    .interaction_options
182                    .iter()
183                    .map(|(input, action, color)| {
184                        let output = if let Some(input) = input {
185                            let input_str = icon_utils::get_controller_input_string(
186                                *input,
187                                &self.global_state.settings,
188                                self.global_state.window.controller_type(),
189                            );
190
191                            match input_str {
192                                Some(binding) => format!("{} {action}", binding),
193                                None => format!("{} {action}", icon_utils::UNBOUND_KEY),
194                            }
195                        } else {
196                            action.to_string()
197                        };
198                        (output, color)
199                    })
200                    .collect(),
201            };
202
203            if state.ids.btns.len() < texts.len() {
204                state.update(|state| {
205                    state
206                        .ids
207                        .btns
208                        .resize(texts.len(), &mut ui.widget_id_generator());
209                })
210            }
211
212            for (idx, (text, color)) in texts.iter().enumerate() {
213                let hints_text = RichText::new(text, self.imgs)
214                    .font_id(self.fonts.cyri.conrod_id)
215                    .font_size(btn_font_size as u32)
216                    .color(**color)
217                    .x_y(0.0, btn_text_pos_y + max_h)
218                    .depth(self.distance_from_player_sqr + 1.0)
219                    .parent(id);
220                let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
221                max_w = max_w.max(w);
222                max_h += h;
223                hints_text.set(state.ids.btns[idx], ui);
224            }
225
226            max_h = max_h.max(btn_rect_size);
227
228            RoundedRectangle::fill_with(
229                [max_w + btn_radius * 2.0, max_h + btn_radius * 2.0],
230                btn_radius,
231                btn_color,
232            )
233            .x_y(0.0, btn_rect_pos_y)
234            .depth(self.distance_from_player_sqr + 2.0)
235            .parent(id)
236            .set(state.ids.btn_bg, ui);
237        }
238        if let Some(collect_failed_data) = self.properties.pickup_failed_pulse {
239            //should never exceed 1.0, but just in case
240            let age = ((self.pulse - collect_failed_data.pulse) / PICKUP_FAILED_FADE_OUT_TIME)
241                .clamp(0.0, 1.0);
242
243            let alpha = 1.0 - age.powi(4);
244            let brightness = 1.0 / (age / 0.07 - 1.0).abs().clamp(0.01, 1.0);
245            let shade_color = |color: Color| {
246                let color::Hsla(hue, sat, lum, alp) = color.to_hsl();
247                color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha)
248            };
249
250            let text = match collect_failed_data.reason {
251                HudCollectFailedReason::InventoryFull => {
252                    self.localized_strings.get_msg("hud-inventory_full")
253                },
254                HudCollectFailedReason::LootOwned { owner, expiry_secs } => {
255                    let owner_name = match owner {
256                        HudLootOwner::Name(name) => {
257                            Cow::Owned(self.localized_strings.get_content(&name))
258                        },
259                        HudLootOwner::Group => self.localized_strings.get_msg("hud-another_group"),
260                        HudLootOwner::Unknown => self.localized_strings.get_msg("hud-someone_else"),
261                    };
262                    self.localized_strings.get_msg_ctx(
263                        "hud-owned_by_for_secs",
264                        &i18n::fluent_args! {
265                            "name" => owner_name,
266                            "secs" => expiry_secs,
267                        },
268                    )
269                },
270            };
271
272            Text::new(&text)
273                .font_id(self.fonts.cyri.conrod_id)
274                .font_size(inv_full_font_size as u32)
275                .color(shade_color(Color::Rgba(0.0, 0.0, 0.0, 1.0)))
276                .x_y(-1.0, inv_full_pos_y - 2.0)
277                .parent(id)
278                .depth(self.distance_from_player_sqr + 6.0)
279                .set(state.ids.inv_full_bg, ui);
280
281            Text::new(&text)
282                .font_id(self.fonts.cyri.conrod_id)
283                .font_size(inv_full_font_size as u32)
284                .color(shade_color(Color::Rgba(1.0, 0.0, 0.0, 1.0)))
285                .x_y(0.0, inv_full_pos_y)
286                .parent(id)
287                .depth(self.distance_from_player_sqr + 5.0)
288                .set(state.ids.inv_full, ui);
289        }
290    }
291}