Skip to main content

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 = format!("<{}>", localized_strings.get_msg("buff-remove"));
287                    let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove);
288                    // Timer overlay
289                    if Button::image(self.get_duration_image(duration_percentage))
290                        .w_h(40.0, 40.0)
291                        .middle_of(*id)
292                        .with_tooltip(
293                            self.tooltip_manager,
294                            &title,
295                            &desc,
296                            &buffs_tooltip,
297                            BUFF_COLOR,
298                        )
299                        .set(*timer_id, ui)
300                        .was_clicked()
301                    {
302                        match buff.kind {
303                            BuffIconKind::Buff { kind, .. } => event.push(Event::RemoveBuff(kind)),
304                            BuffIconKind::Stance(_) => event.push(Event::LeaveStance),
305                        }
306                    };
307                });
308
309            // Create Debuff Widgets
310            let mut debuff_vec = state
311                .ids
312                .debuffs
313                .iter()
314                .copied()
315                .zip(state.ids.debuff_timers.iter().copied())
316                .zip(state.ids.debuff_multiplicities.chunks(2))
317                .zip(buff_icons.iter().filter(|info| !info.is_buff))
318                .collect::<Vec<_>>();
319
320            // Sort the debuffs by kind
321            debuff_vec.sort_by_key(|(((_id, _timer_id), _mult_id), debuff)| debuff.kind);
322
323            debuff_vec
324                .iter()
325                .enumerate()
326                .for_each(|(i, (((id, timer_id), mult_id), debuff))| {
327                    let max_duration = debuff.kind.max_duration();
328                    let current_duration = debuff.end_time.map(|end| end - self.time.0);
329                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
330                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
331                    }) as u32; // Percentage to determine which frame of the timer overlay is displayed
332                    let debuff_img = debuff.kind.image(self.imgs);
333                    let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0);
334                    // Sort buffs into rows of 11 slots
335                    let x = i % 6;
336                    let y = i / 6;
337                    let debuff_widget = debuff_widget.bottom_right_with_margins_on(
338                        state.ids.debuffs_align,
339                        0.0 + y as f64 * (41.0),
340                        1.5 + x as f64 * (43.0),
341                    );
342
343                    debuff_widget
344                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
345                            Some(pulsating_col)
346                        } else {
347                            Some(norm_col)
348                        })
349                        .set(*id, ui);
350                    if debuff.multiplicity() > 1 {
351                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
352                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
353                            .wh_of(mult_id[1])
354                            .graphics_for(*id)
355                            .set(mult_id[0], ui);
356                        Text::new(&format!("{}", debuff.multiplicity()))
357                            .middle_of(mult_id[0])
358                            .graphics_for(*id)
359                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
360                            .font_id(self.fonts.cyri.conrod_id)
361                            .color(MULTIPLICITY_COLOR)
362                            .set(mult_id[1], ui);
363                    }
364                    // Create Debuff tooltip
365                    let (title, desc_txt) = debuff.kind.title_description(localized_strings);
366                    let remaining_time = debuff.get_buff_time(*self.time);
367                    let desc = format!("{}\n\n{}", desc_txt, remaining_time);
368                    Image::new(self.get_duration_image(duration_percentage))
369                        .w_h(40.0, 40.0)
370                        .middle_of(*id)
371                        .with_tooltip(
372                            self.tooltip_manager,
373                            &title,
374                            &desc,
375                            &buffs_tooltip,
376                            DEBUFF_COLOR,
377                        )
378                        .set(*timer_id, ui);
379                });
380        }
381
382        if let BuffPosition::Map = buff_position {
383            // Alignment
384            Rectangle::fill_with([210.0, 210.0], color::TRANSPARENT)
385                .top_right_with_margins_on(ui.window, 5.0, 270.0)
386                .set(state.ids.align, ui);
387
388            // Buffs and Debuffs
389            let buff_count = buff_icons.len().min(11);
390            // Limit displayed buffs
391            let buff_count = buff_count.min(20);
392
393            let generator = &mut ui.widget_id_generator();
394            if state.ids.buffs.len() < buff_count {
395                state.update(|state| state.ids.buffs.resize(buff_count, generator));
396            };
397            if state.ids.buff_timers.len() < buff_count {
398                state.update(|state| state.ids.buff_timers.resize(buff_count, generator));
399            };
400            if state.ids.buff_txts.len() < buff_count {
401                state.update(|state| state.ids.buff_txts.resize(buff_count, generator));
402            };
403            if state.ids.buff_multiplicities.len() < 2 * buff_count {
404                state.update(|state| {
405                    state
406                        .ids
407                        .buff_multiplicities
408                        .resize(2 * buff_count, generator)
409                });
410            };
411
412            // Create Buff Widgets
413
414            let mut buff_vec = state
415                .ids
416                .buffs
417                .iter()
418                .copied()
419                .zip(state.ids.buff_timers.iter().copied())
420                .zip(state.ids.buff_txts.iter().copied())
421                .zip(state.ids.buff_multiplicities.chunks(2))
422                .zip(buff_icons.iter())
423                .collect::<Vec<_>>();
424
425            // Sort the buffs by kind
426            buff_vec.sort_by_key(|((_id, _timer_id), txt_id)| std::cmp::Reverse(txt_id.kind));
427
428            buff_vec.iter().enumerate().for_each(
429                |(i, ((((id, timer_id), txt_id), mult_id), buff))| {
430                    let max_duration = buff.kind.max_duration();
431                    let current_duration = buff.end_time.map(|end| end - self.time.0);
432                    // Percentage to determine which frame of the timer overlay is displayed
433                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
434                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
435                    }) as u32;
436                    let buff_img = buff.kind.image(self.imgs);
437                    let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
438                    // Sort buffs into rows of 6 slots
439                    let x = i % 6;
440                    let y = i / 6;
441                    let buff_widget = buff_widget.top_right_with_margins_on(
442                        state.ids.align,
443                        0.0 + y as f64 * (54.0),
444                        0.0 + x as f64 * (42.0),
445                    );
446                    buff_widget
447                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
448                            Some(pulsating_col)
449                        } else {
450                            Some(norm_col)
451                        })
452                        .set(*id, ui);
453                    if buff.multiplicity() > 1 {
454                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
455                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
456                            .wh_of(mult_id[1])
457                            .graphics_for(*id)
458                            .set(mult_id[0], ui);
459                        Text::new(&format!("{}", buff.multiplicity()))
460                            .middle_of(mult_id[0])
461                            .graphics_for(*id)
462                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
463                            .font_id(self.fonts.cyri.conrod_id)
464                            .color(MULTIPLICITY_COLOR)
465                            .set(mult_id[1], ui);
466                    }
467                    // Create Buff tooltip
468                    let (title, desc_txt) = buff.kind.title_description(localized_strings);
469                    let remaining_time = buff.get_buff_time(*self.time);
470                    let click_to_remove = format!("<{}>", localized_strings.get_msg("buff-remove"));
471                    let desc = if buff.is_buff {
472                        format!("{}\n\n{}", desc_txt, click_to_remove)
473                    } else {
474                        desc_txt.to_string()
475                    };
476                    // Timer overlay
477                    if Button::image(self.get_duration_image(duration_percentage))
478                        .w_h(40.0, 40.0)
479                        .middle_of(*id)
480                        .with_tooltip(
481                            self.tooltip_manager,
482                            &title,
483                            &desc,
484                            &buffs_tooltip,
485                            if buff.is_buff {
486                                BUFF_COLOR
487                            } else {
488                                DEBUFF_COLOR
489                            },
490                        )
491                        .set(*timer_id, ui)
492                        .was_clicked()
493                    {
494                        match buff.kind {
495                            BuffIconKind::Buff { kind, .. } => event.push(Event::RemoveBuff(kind)),
496                            BuffIconKind::Stance(_) => event.push(Event::LeaveStance),
497                        }
498                    }
499                    Text::new(&remaining_time)
500                        .down_from(*timer_id, 1.0)
501                        .font_size(self.fonts.cyri.scale(10))
502                        .font_id(self.fonts.cyri.conrod_id)
503                        .graphics_for(*timer_id)
504                        .color(TEXT_COLOR)
505                        .set(*txt_id, ui);
506                },
507            );
508        }
509        event
510    }
511}
512
513impl BuffsBar<'_> {
514    fn get_duration_image(&self, duration_percentage: u32) -> Id {
515        match duration_percentage as u64 {
516            875..=1000 => self.imgs.nothing, // 8/8
517            750..=874 => self.imgs.buff_0,   // 7/8
518            625..=749 => self.imgs.buff_1,   // 6/8
519            500..=624 => self.imgs.buff_2,   // 5/8
520            375..=499 => self.imgs.buff_3,   // 4/8
521            250..=374 => self.imgs.buff_4,   // 3/8
522            125..=249 => self.imgs.buff_5,   // 2/8
523            0..=124 => self.imgs.buff_6,     // 1/8
524            _ => self.imgs.nothing,
525        }
526    }
527}