veloren_voxygen/hud/
overitem.rs

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