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