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 gen = &mut ui.widget_id_generator();
185            if state.ids.buffs.len() < buff_count {
186                state.update(|state| state.ids.buffs.resize(buff_count, gen));
187            };
188            if state.ids.debuffs.len() < debuff_count {
189                state.update(|state| state.ids.debuffs.resize(debuff_count, gen));
190            };
191            if state.ids.buff_timers.len() < buff_count {
192                state.update(|state| state.ids.buff_timers.resize(buff_count, gen));
193            };
194            if state.ids.debuff_timers.len() < debuff_count {
195                state.update(|state| state.ids.debuff_timers.resize(debuff_count, gen));
196            };
197            if state.ids.buff_multiplicities.len() < 2 * buff_count {
198                state.update(|state| state.ids.buff_multiplicities.resize(2 * buff_count, gen));
199            };
200            if state.ids.debuff_multiplicities.len() < 2 * debuff_count {
201                state.update(|state| {
202                    state
203                        .ids
204                        .debuff_multiplicities
205                        .resize(2 * debuff_count, gen)
206                });
207            };
208
209            // Create Buff Widgets
210            let mut buff_vec = state
211                .ids
212                .buffs
213                .iter()
214                .copied()
215                .zip(state.ids.buff_timers.iter().copied())
216                .zip(state.ids.buff_multiplicities.chunks(2))
217                .zip(buff_icons.iter().filter(|info| info.is_buff))
218                .collect::<Vec<_>>();
219
220            // Sort the buffs by kind
221            buff_vec
222                .sort_by_key(|(((_id, _timer_id), _mult_id), buff)| std::cmp::Reverse(buff.kind));
223
224            buff_vec
225                .iter()
226                .enumerate()
227                .for_each(|(i, (((id, timer_id), mult_id), buff))| {
228                    let max_duration = buff.kind.max_duration();
229                    let current_duration = buff.end_time.map(|end| end - self.time.0);
230                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
231                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
232                    }) as u32; // Percentage to determine which frame of the timer overlay is displayed
233                    let buff_img = buff.kind.image(self.imgs);
234                    let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
235                    // Sort buffs into rows of 11 slots
236                    let x = i % 6;
237                    let y = i / 6;
238                    let buff_widget = buff_widget.bottom_left_with_margins_on(
239                        state.ids.buffs_align,
240                        0.0 + y as f64 * (41.0),
241                        1.5 + x as f64 * (43.0),
242                    );
243
244                    buff_widget
245                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
246                            Some(pulsating_col)
247                        } else {
248                            Some(norm_col)
249                        })
250                        .set(*id, ui);
251                    if buff.multiplicity() > 1 {
252                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
253                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
254                            .wh_of(mult_id[1])
255                            .graphics_for(*id)
256                            .set(mult_id[0], ui);
257                        Text::new(&format!("{}", buff.multiplicity()))
258                            .middle_of(mult_id[0])
259                            .graphics_for(*id)
260                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
261                            .font_id(self.fonts.cyri.conrod_id)
262                            .color(MULTIPLICITY_COLOR)
263                            .set(mult_id[1], ui);
264                    }
265                    // Create Buff tooltip
266                    let (title, desc_txt) = buff.kind.title_description(localized_strings);
267                    let remaining_time = buff.get_buff_time(*self.time);
268                    let click_to_remove =
269                        format!("<{}>", &localized_strings.get_msg("buff-remove"));
270                    let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove);
271                    // Timer overlay
272                    if Button::image(self.get_duration_image(duration_percentage))
273                        .w_h(40.0, 40.0)
274                        .middle_of(*id)
275                        .with_tooltip(
276                            self.tooltip_manager,
277                            &title,
278                            &desc,
279                            &buffs_tooltip,
280                            BUFF_COLOR,
281                        )
282                        .set(*timer_id, ui)
283                        .was_clicked()
284                    {
285                        match buff.kind {
286                            BuffIconKind::Buff { kind, .. } => event.push(Event::RemoveBuff(kind)),
287                            BuffIconKind::Stance(_) => event.push(Event::LeaveStance),
288                        }
289                    };
290                });
291
292            // Create Debuff Widgets
293            let mut debuff_vec = state
294                .ids
295                .debuffs
296                .iter()
297                .copied()
298                .zip(state.ids.debuff_timers.iter().copied())
299                .zip(state.ids.debuff_multiplicities.chunks(2))
300                .zip(buff_icons.iter().filter(|info| !info.is_buff))
301                .collect::<Vec<_>>();
302
303            // Sort the debuffs by kind
304            debuff_vec.sort_by_key(|(((_id, _timer_id), _mult_id), debuff)| debuff.kind);
305
306            debuff_vec
307                .iter()
308                .enumerate()
309                .for_each(|(i, (((id, timer_id), mult_id), debuff))| {
310                    let max_duration = debuff.kind.max_duration();
311                    let current_duration = debuff.end_time.map(|end| end - self.time.0);
312                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
313                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
314                    }) as u32; // Percentage to determine which frame of the timer overlay is displayed
315                    let debuff_img = debuff.kind.image(self.imgs);
316                    let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0);
317                    // Sort buffs into rows of 11 slots
318                    let x = i % 6;
319                    let y = i / 6;
320                    let debuff_widget = debuff_widget.bottom_right_with_margins_on(
321                        state.ids.debuffs_align,
322                        0.0 + y as f64 * (41.0),
323                        1.5 + x as f64 * (43.0),
324                    );
325
326                    debuff_widget
327                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
328                            Some(pulsating_col)
329                        } else {
330                            Some(norm_col)
331                        })
332                        .set(*id, ui);
333                    if debuff.multiplicity() > 1 {
334                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
335                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
336                            .wh_of(mult_id[1])
337                            .graphics_for(*id)
338                            .set(mult_id[0], ui);
339                        Text::new(&format!("{}", debuff.multiplicity()))
340                            .middle_of(mult_id[0])
341                            .graphics_for(*id)
342                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
343                            .font_id(self.fonts.cyri.conrod_id)
344                            .color(MULTIPLICITY_COLOR)
345                            .set(mult_id[1], ui);
346                    }
347                    // Create Debuff tooltip
348                    let (title, desc_txt) = debuff.kind.title_description(localized_strings);
349                    let remaining_time = debuff.get_buff_time(*self.time);
350                    let desc = format!("{}\n\n{}", desc_txt, remaining_time);
351                    Image::new(self.get_duration_image(duration_percentage))
352                        .w_h(40.0, 40.0)
353                        .middle_of(*id)
354                        .with_tooltip(
355                            self.tooltip_manager,
356                            &title,
357                            &desc,
358                            &buffs_tooltip,
359                            DEBUFF_COLOR,
360                        )
361                        .set(*timer_id, ui);
362                });
363        }
364
365        if let BuffPosition::Map = buff_position {
366            // Alignment
367            Rectangle::fill_with([210.0, 210.0], color::TRANSPARENT)
368                .top_right_with_margins_on(ui.window, 5.0, 270.0)
369                .set(state.ids.align, ui);
370
371            // Buffs and Debuffs
372            let buff_count = buff_icons.len().min(11);
373            // Limit displayed buffs
374            let buff_count = buff_count.min(20);
375
376            let gen = &mut ui.widget_id_generator();
377            if state.ids.buffs.len() < buff_count {
378                state.update(|state| state.ids.buffs.resize(buff_count, gen));
379            };
380            if state.ids.buff_timers.len() < buff_count {
381                state.update(|state| state.ids.buff_timers.resize(buff_count, gen));
382            };
383            if state.ids.buff_txts.len() < buff_count {
384                state.update(|state| state.ids.buff_txts.resize(buff_count, gen));
385            };
386            if state.ids.buff_multiplicities.len() < 2 * buff_count {
387                state.update(|state| state.ids.buff_multiplicities.resize(2 * buff_count, gen));
388            };
389
390            // Create Buff Widgets
391
392            let mut buff_vec = state
393                .ids
394                .buffs
395                .iter()
396                .copied()
397                .zip(state.ids.buff_timers.iter().copied())
398                .zip(state.ids.buff_txts.iter().copied())
399                .zip(state.ids.buff_multiplicities.chunks(2))
400                .zip(buff_icons.iter())
401                .collect::<Vec<_>>();
402
403            // Sort the buffs by kind
404            buff_vec.sort_by_key(|((_id, _timer_id), txt_id)| std::cmp::Reverse(txt_id.kind));
405
406            buff_vec.iter().enumerate().for_each(
407                |(i, ((((id, timer_id), txt_id), mult_id), buff))| {
408                    let max_duration = buff.kind.max_duration();
409                    let current_duration = buff.end_time.map(|end| end - self.time.0);
410                    // Percentage to determine which frame of the timer overlay is displayed
411                    let duration_percentage = current_duration.map_or(1000.0, |cur| {
412                        max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
413                    }) as u32;
414                    let buff_img = buff.kind.image(self.imgs);
415                    let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
416                    // Sort buffs into rows of 6 slots
417                    let x = i % 6;
418                    let y = i / 6;
419                    let buff_widget = buff_widget.top_right_with_margins_on(
420                        state.ids.align,
421                        0.0 + y as f64 * (54.0),
422                        0.0 + x as f64 * (42.0),
423                    );
424                    buff_widget
425                        .color(if current_duration.is_some_and(|cur| cur < 10.0) {
426                            Some(pulsating_col)
427                        } else {
428                            Some(norm_col)
429                        })
430                        .set(*id, ui);
431                    if buff.multiplicity() > 1 {
432                        Rectangle::fill_with([0.0, 0.0], MULTIPLICITY_COLOR.plain_contrast())
433                            .bottom_right_with_margins_on(*id, 1.0, 1.0)
434                            .wh_of(mult_id[1])
435                            .graphics_for(*id)
436                            .set(mult_id[0], ui);
437                        Text::new(&format!("{}", buff.multiplicity()))
438                            .middle_of(mult_id[0])
439                            .graphics_for(*id)
440                            .font_size(self.fonts.cyri.scale(MULTIPLICITY_FONT_SIZE))
441                            .font_id(self.fonts.cyri.conrod_id)
442                            .color(MULTIPLICITY_COLOR)
443                            .set(mult_id[1], ui);
444                    }
445                    // Create Buff tooltip
446                    let (title, desc_txt) = buff.kind.title_description(localized_strings);
447                    let remaining_time = buff.get_buff_time(*self.time);
448                    let click_to_remove =
449                        format!("<{}>", &localized_strings.get_msg("buff-remove"));
450                    let desc = if buff.is_buff {
451                        format!("{}\n\n{}", desc_txt, click_to_remove)
452                    } else {
453                        desc_txt.to_string()
454                    };
455                    // Timer overlay
456                    if Button::image(self.get_duration_image(duration_percentage))
457                        .w_h(40.0, 40.0)
458                        .middle_of(*id)
459                        .with_tooltip(
460                            self.tooltip_manager,
461                            &title,
462                            &desc,
463                            &buffs_tooltip,
464                            if buff.is_buff {
465                                BUFF_COLOR
466                            } else {
467                                DEBUFF_COLOR
468                            },
469                        )
470                        .set(*timer_id, ui)
471                        .was_clicked()
472                    {
473                        match buff.kind {
474                            BuffIconKind::Buff { kind, .. } => event.push(Event::RemoveBuff(kind)),
475                            BuffIconKind::Stance(_) => event.push(Event::LeaveStance),
476                        }
477                    }
478                    Text::new(&remaining_time)
479                        .down_from(*timer_id, 1.0)
480                        .font_size(self.fonts.cyri.scale(10))
481                        .font_id(self.fonts.cyri.conrod_id)
482                        .graphics_for(*timer_id)
483                        .color(TEXT_COLOR)
484                        .set(*txt_id, ui);
485                },
486            );
487        }
488        event
489    }
490}
491
492impl BuffsBar<'_> {
493    fn get_duration_image(&self, duration_percentage: u32) -> Id {
494        match duration_percentage as u64 {
495            875..=1000 => self.imgs.nothing, // 8/8
496            750..=874 => self.imgs.buff_0,   // 7/8
497            625..=749 => self.imgs.buff_1,   // 6/8
498            500..=624 => self.imgs.buff_2,   // 5/8
499            375..=499 => self.imgs.buff_3,   // 4/8
500            250..=374 => self.imgs.buff_4,   // 3/8
501            125..=249 => self.imgs.buff_5,   // 2/8
502            0..=124 => self.imgs.buff_6,     // 1/8
503            _ => self.imgs.nothing,
504        }
505    }
506}