veloren_voxygen/hud/
buffs.rs

1use super::{
2    BUFF_COLOR, DEBUFF_COLOR, TEXT_COLOR,
3    img_ids::{Imgs, ImgsRot},
4};
5use crate::{
6    GlobalState,
7    hud::{BuffIcon, BuffIconKind, BuffPosition, animation::animation_timer},
8    ui::{ImageFrame, Tooltip, TooltipManager, Tooltipable, fonts::Fonts},
9};
10use i18n::Localization;
11
12use common::{
13    comp::{BuffKind, Buffs, Energy, Health, Stance},
14    resources::Time,
15};
16use conrod_core::{
17    Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
18    image::Id,
19    widget::{self, Button, Image, Rectangle, Text},
20    widget_ids,
21};
22
23widget_ids! {
24    struct Ids {
25        align,
26        buffs_align,
27        debuffs_align,
28        buff_test,
29        debuff_test,
30        buffs[],
31        buff_timers[],
32        debuffs[],
33        debuff_timers[],
34        buff_txts[],
35        buff_multiplicities[],
36        debuff_multiplicities[],
37    }
38}
39
40#[derive(WidgetCommon)]
41pub struct BuffsBar<'a> {
42    imgs: &'a Imgs,
43    fonts: &'a Fonts,
44    #[conrod(common_builder)]
45    common: widget::CommonBuilder,
46    rot_imgs: &'a ImgsRot,
47    tooltip_manager: &'a mut TooltipManager,
48    localized_strings: &'a Localization,
49    buffs: &'a Buffs,
50    stance: Option<&'a Stance>,
51    pulse: f32,
52    global_state: &'a GlobalState,
53    health: &'a Health,
54    energy: &'a Energy,
55    time: &'a Time,
56}
57
58impl<'a> BuffsBar<'a> {
59    pub fn new(
60        imgs: &'a Imgs,
61        fonts: &'a Fonts,
62        rot_imgs: &'a ImgsRot,
63        tooltip_manager: &'a mut TooltipManager,
64        localized_strings: &'a Localization,
65        buffs: &'a Buffs,
66        stance: Option<&'a Stance>,
67        pulse: f32,
68        global_state: &'a GlobalState,
69        health: &'a Health,
70        energy: &'a Energy,
71        time: &'a Time,
72    ) -> Self {
73        Self {
74            imgs,
75            fonts,
76            common: widget::CommonBuilder::default(),
77            rot_imgs,
78            tooltip_manager,
79            localized_strings,
80            buffs,
81            stance,
82            pulse,
83            global_state,
84            health,
85            energy,
86            time,
87        }
88    }
89}
90
91pub struct State {
92    ids: Ids,
93}
94
95pub enum Event {
96    RemoveBuff(BuffKind),
97    LeaveStance,
98}
99
100const MULTIPLICITY_COLOR: Color = TEXT_COLOR;
101const MULTIPLICITY_FONT_SIZE: u32 = 20;
102
103impl Widget for BuffsBar<'_> {
104    type Event = Vec<Event>;
105    type State = State;
106    type Style = ();
107
108    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
109        State {
110            ids: Ids::new(id_gen),
111        }
112    }
113
114    fn style(&self) -> Self::Style {}
115
116    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
117        common_base::prof_span!("BuffsBar::update");
118        let widget::UpdateArgs { state, ui, .. } = args;
119        let mut event = Vec::new();
120        let localized_strings = self.localized_strings;
121        let buff_ani = animation_timer(self.pulse) + 0.5; //Animation timer
122        let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
123        let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
124        let buff_position = self.global_state.settings.interface.buff_position;
125        let buffs_tooltip = Tooltip::new({
126            // Edge images [t, b, r, l]
127            // Corner images [tr, tl, br, bl]
128            let edge = &self.rot_imgs.tt_side;
129            let corner = &self.rot_imgs.tt_corner;
130            ImageFrame::new(
131                [edge.cw180, edge.none, edge.cw270, edge.cw90],
132                [corner.none, corner.cw270, corner.cw90, corner.cw180],
133                Color::Rgba(0.08, 0.07, 0.04, 1.0),
134                5.0,
135            )
136        })
137        .title_font_size(self.fonts.cyri.scale(15))
138        .parent(ui.window)
139        .desc_font_size(self.fonts.cyri.scale(12))
140        .font_id(self.fonts.cyri.conrod_id)
141        .desc_text_color(TEXT_COLOR);
142        let buff_icons = BuffIcon::icons_vec(self.buffs, self.stance);
143        if let BuffPosition::Bar = buff_position {
144            let decayed_health = 1.0 - self.health.maximum() / self.health.base_max();
145            let show_health = self.global_state.settings.interface.always_show_bars
146                || (self.health.current() - self.health.maximum()).abs() > Health::HEALTH_EPSILON
147                || decayed_health > 0.0;
148            let show_energy = self.global_state.settings.interface.always_show_bars
149                || (self.energy.current() - self.energy.maximum()).abs() > Energy::ENERGY_EPSILON;
150            let offset = if show_energy && show_health {
151                140.0
152            } else if show_health || show_energy {
153                95.0
154            } else {
155                55.0
156            };
157            // Alignment
158            Rectangle::fill_with([484.0, 100.0], color::TRANSPARENT)
159                .mid_bottom_with_margin_on(ui.window, offset)
160                .set(state.ids.align, ui);
161            Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
162                .bottom_left_with_margins_on(state.ids.align, 0.0, 0.0)
163                .set(state.ids.debuffs_align, ui);
164            Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT)
165                .bottom_right_with_margins_on(state.ids.align, 0.0, 0.0)
166                .set(state.ids.buffs_align, ui);
167
168            // Buffs and Debuffs
169            let (buff_count, debuff_count) =
170                buff_icons
171                    .iter()
172                    .fold((0, 0), |(buff_count, debuff_count), info| {
173                        if info.is_buff {
174                            (buff_count + 1, debuff_count)
175                        } else {
176                            (buff_count, debuff_count + 1)
177                        }
178                    });
179
180            // Limit displayed buffs
181            let buff_count = buff_count.min(12);
182            let debuff_count = debuff_count.min(12);
183
184            let generator = &mut ui.widget_id_generator();
185            if state.ids.buffs.len() < buff_count {
186                state.update(|state| state.ids.buffs.resize(buff_count, generator));
187            };
188            if state.ids.debuffs.len() < debuff_count {
189                state.update(|state| state.ids.debuffs.resize(debuff_count, generator));
190            };
191            if state.ids.buff_timers.len() < buff_count {
192                state.update(|state| state.ids.buff_timers.resize(buff_count, generator));
193            };
194            if state.ids.debuff_timers.len() < debuff_count {
195                state.update(|state| state.ids.debuff_timers.resize(debuff_count, generator));
196            };
197            if state.ids.buff_multiplicities.len() < 2 * buff_count {
198                state.update(|state| {
199                    state
200                        .ids
201                        .buff_multiplicities
202                        .resize(2 * buff_count, generator)
203                });
204            };
205            if state.ids.debuff_multiplicities.len() < 2 * debuff_count {
206                state.update(|state| {
207                    state
208                        .ids
209                        .debuff_multiplicities
210                        .resize(2 * debuff_count, generator)
211                });
212            };
213
214            // Create Buff Widgets
215            let mut buff_vec = state
216                .ids
217                .buffs
218                .iter()
219                .copied()
220                .zip(state.ids.buff_timers.iter().copied())
221                .zip(state.ids.buff_multiplicities.chunks(2))
222                .zip(buff_icons.iter().filter(|info| info.is_buff))
223                .collect::<Vec<_>>();
224
225            // Sort the buffs by kind
226            buff_vec
227                .sort_by_key(|(((_id, _timer_id), _mult_id), buff)| std::cmp::Reverse(buff.kind));
228
229            buff_vec
230                .iter()
231                .enumerate()
232                .for_each(|(i, (((id, timer_id), mult_id), buff))| {
233                    let max_duration = buff.kind.max_duration();
234                    let current_duration = buff.end_time.map(|end| end - self.time.0);
235                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
236                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
237                    }) as u32; // Percentage to determine which frame of the timer overlay is displayed
238                    let buff_img = buff.kind.image(self.imgs);
239                    let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
240                    // Sort buffs into rows of 11 slots
241                    let x = i % 6;
242                    let y = i / 6;
243                    let buff_widget = buff_widget.bottom_left_with_margins_on(
244                        state.ids.buffs_align,
245                        0.0 + y as f64 * (41.0),
246                        1.5 + x as f64 * (43.0),
247                    );
248
249                    buff_widget
250                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
251                            Some(pulsating_col)
252                        } else {
253                            Some(norm_col)
254                        })
255                        .set(*id, ui);
256                    if buff.multiplicity() > 1 {
257                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
258                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
259                            .wh_of(mult_id[1])
260                            .graphics_for(*id)
261                            .set(mult_id[0], ui);
262                        Text::new(&format!("{}", buff.multiplicity()))
263                            .middle_of(mult_id[0])
264                            .graphics_for(*id)
265                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
266                            .font_id(self.fonts.cyri.conrod_id)
267                            .color(MULTIPLICITY_COLOR)
268                            .set(mult_id[1], ui);
269                    }
270                    // Create Buff tooltip
271                    let (title, desc_txt) = buff.kind.title_description(localized_strings);
272                    let remaining_time = buff.get_buff_time(*self.time);
273                    let click_to_remove =
274                        format!("<{}>", &localized_strings.get_msg("buff-remove"));
275                    let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove);
276                    // Timer overlay
277                    if Button::image(self.get_duration_image(duration_percentage))
278                        .w_h(40.0, 40.0)
279                        .middle_of(*id)
280                        .with_tooltip(
281                            self.tooltip_manager,
282                            &title,
283                            &desc,
284                            &buffs_tooltip,
285                            BUFF_COLOR,
286                        )
287                        .set(*timer_id, ui)
288                        .was_clicked()
289                    {
290                        match buff.kind {
291                            BuffIconKind::Buff { kind, .. } => event.push(Event::RemoveBuff(kind)),
292                            BuffIconKind::Stance(_) => event.push(Event::LeaveStance),
293                        }
294                    };
295                });
296
297            // Create Debuff Widgets
298            let mut debuff_vec = state
299                .ids
300                .debuffs
301                .iter()
302                .copied()
303                .zip(state.ids.debuff_timers.iter().copied())
304                .zip(state.ids.debuff_multiplicities.chunks(2))
305                .zip(buff_icons.iter().filter(|info| !info.is_buff))
306                .collect::<Vec<_>>();
307
308            // Sort the debuffs by kind
309            debuff_vec.sort_by_key(|(((_id, _timer_id), _mult_id), debuff)| debuff.kind);
310
311            debuff_vec
312                .iter()
313                .enumerate()
314                .for_each(|(i, (((id, timer_id), mult_id), debuff))| {
315                    let max_duration = debuff.kind.max_duration();
316                    let current_duration = debuff.end_time.map(|end| end - self.time.0);
317                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
318                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
319                    }) as u32; // Percentage to determine which frame of the timer overlay is displayed
320                    let debuff_img = debuff.kind.image(self.imgs);
321                    let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0);
322                    // Sort buffs into rows of 11 slots
323                    let x = i % 6;
324                    let y = i / 6;
325                    let debuff_widget = debuff_widget.bottom_right_with_margins_on(
326                        state.ids.debuffs_align,
327                        0.0 + y as f64 * (41.0),
328                        1.5 + x as f64 * (43.0),
329                    );
330
331                    debuff_widget
332                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
333                            Some(pulsating_col)
334                        } else {
335                            Some(norm_col)
336                        })
337                        .set(*id, ui);
338                    if debuff.multiplicity() > 1 {
339                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
340                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
341                            .wh_of(mult_id[1])
342                            .graphics_for(*id)
343                            .set(mult_id[0], ui);
344                        Text::new(&format!("{}", debuff.multiplicity()))
345                            .middle_of(mult_id[0])
346                            .graphics_for(*id)
347                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
348                            .font_id(self.fonts.cyri.conrod_id)
349                            .color(MULTIPLICITY_COLOR)
350                            .set(mult_id[1], ui);
351                    }
352                    // Create Debuff tooltip
353                    let (title, desc_txt) = debuff.kind.title_description(localized_strings);
354                    let remaining_time = debuff.get_buff_time(*self.time);
355                    let desc = format!("{}\n\n{}", desc_txt, remaining_time);
356                    Image::new(self.get_duration_image(duration_percentage))
357                        .w_h(40.0, 40.0)
358                        .middle_of(*id)
359                        .with_tooltip(
360                            self.tooltip_manager,
361                            &title,
362                            &desc,
363                            &buffs_tooltip,
364                            DEBUFF_COLOR,
365                        )
366                        .set(*timer_id, ui);
367                });
368        }
369
370        if let BuffPosition::Map = buff_position {
371            // Alignment
372            Rectangle::fill_with([210.0, 210.0], color::TRANSPARENT)
373                .top_right_with_margins_on(ui.window, 5.0, 270.0)
374                .set(state.ids.align, ui);
375
376            // Buffs and Debuffs
377            let buff_count = buff_icons.len().min(11);
378            // Limit displayed buffs
379            let buff_count = buff_count.min(20);
380
381            let generator = &mut ui.widget_id_generator();
382            if state.ids.buffs.len() < buff_count {
383                state.update(|state| state.ids.buffs.resize(buff_count, generator));
384            };
385            if state.ids.buff_timers.len() < buff_count {
386                state.update(|state| state.ids.buff_timers.resize(buff_count, generator));
387            };
388            if state.ids.buff_txts.len() < buff_count {
389                state.update(|state| state.ids.buff_txts.resize(buff_count, generator));
390            };
391            if state.ids.buff_multiplicities.len() < 2 * buff_count {
392                state.update(|state| {
393                    state
394                        .ids
395                        .buff_multiplicities
396                        .resize(2 * buff_count, generator)
397                });
398            };
399
400            // Create Buff Widgets
401
402            let mut buff_vec = state
403                .ids
404                .buffs
405                .iter()
406                .copied()
407                .zip(state.ids.buff_timers.iter().copied())
408                .zip(state.ids.buff_txts.iter().copied())
409                .zip(state.ids.buff_multiplicities.chunks(2))
410                .zip(buff_icons.iter())
411                .collect::<Vec<_>>();
412
413            // Sort the buffs by kind
414            buff_vec.sort_by_key(|((_id, _timer_id), txt_id)| std::cmp::Reverse(txt_id.kind));
415
416            buff_vec.iter().enumerate().for_each(
417                |(i, ((((id, timer_id), txt_id), mult_id), buff))| {
418                    let max_duration = buff.kind.max_duration();
419                    let current_duration = buff.end_time.map(|end| end - self.time.0);
420                    // Percentage to determine which frame of the timer overlay is displayed
421                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
422                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
423                    }) as u32;
424                    let buff_img = buff.kind.image(self.imgs);
425                    let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
426                    // Sort buffs into rows of 6 slots
427                    let x = i % 6;
428                    let y = i / 6;
429                    let buff_widget = buff_widget.top_right_with_margins_on(
430                        state.ids.align,
431                        0.0 + y as f64 * (54.0),
432                        0.0 + x as f64 * (42.0),
433                    );
434                    buff_widget
435                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
436                            Some(pulsating_col)
437                        } else {
438                            Some(norm_col)
439                        })
440                        .set(*id, ui);
441                    if buff.multiplicity() > 1 {
442                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
443                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
444                            .wh_of(mult_id[1])
445                            .graphics_for(*id)
446                            .set(mult_id[0], ui);
447                        Text::new(&format!("{}", buff.multiplicity()))
448                            .middle_of(mult_id[0])
449                            .graphics_for(*id)
450                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
451                            .font_id(self.fonts.cyri.conrod_id)
452                            .color(MULTIPLICITY_COLOR)
453                            .set(mult_id[1], ui);
454                    }
455                    // Create Buff tooltip
456                    let (title, desc_txt) = buff.kind.title_description(localized_strings);
457                    let remaining_time = buff.get_buff_time(*self.time);
458                    let click_to_remove =
459                        format!("<{}>", &localized_strings.get_msg("buff-remove"));
460                    let desc = if buff.is_buff {
461                        format!("{}\n\n{}", desc_txt, click_to_remove)
462                    } else {
463                        desc_txt.to_string()
464                    };
465                    // Timer overlay
466                    if Button::image(self.get_duration_image(duration_percentage))
467                        .w_h(40.0, 40.0)
468                        .middle_of(*id)
469                        .with_tooltip(
470                            self.tooltip_manager,
471                            &title,
472                            &desc,
473                            &buffs_tooltip,
474                            if buff.is_buff {
475                                BUFF_COLOR
476                            } else {
477                                DEBUFF_COLOR
478                            },
479                        )
480                        .set(*timer_id, ui)
481                        .was_clicked()
482                    {
483                        match buff.kind {
484                            BuffIconKind::Buff { kind, .. } => event.push(Event::RemoveBuff(kind)),
485                            BuffIconKind::Stance(_) => event.push(Event::LeaveStance),
486                        }
487                    }
488                    Text::new(&remaining_time)
489                        .down_from(*timer_id, 1.0)
490                        .font_size(self.fonts.cyri.scale(10))
491                        .font_id(self.fonts.cyri.conrod_id)
492                        .graphics_for(*timer_id)
493                        .color(TEXT_COLOR)
494                        .set(*txt_id, ui);
495                },
496            );
497        }
498        event
499    }
500}
501
502impl BuffsBar<'_> {
503    fn get_duration_image(&self, duration_percentage: u32) -> Id {
504        match duration_percentage as u64 {
505            875..=1000 => self.imgs.nothing, // 8/8
506            750..=874 => self.imgs.buff_0,   // 7/8
507            625..=749 => self.imgs.buff_1,   // 6/8
508            500..=624 => self.imgs.buff_2,   // 5/8
509            375..=499 => self.imgs.buff_3,   // 4/8
510            250..=374 => self.imgs.buff_4,   // 3/8
511            125..=249 => self.imgs.buff_5,   // 2/8
512            0..=124 => self.imgs.buff_6,     // 1/8
513            _ => self.imgs.nothing,
514        }
515    }
516}