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