use super::{ img_ids::{Imgs, ImgsRot}, BUFF_COLOR, DEBUFF_COLOR, TEXT_COLOR, }; use crate::{ hud::{self, BuffPosition}, i18n::Localization, ui::{fonts::Fonts, ImageFrame, Tooltip, TooltipManager, Tooltipable}, GlobalState, }; use common::comp::{BuffKind, Buffs, Energy, Health}; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Text}, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; widget_ids! { struct Ids { align, buffs_align, debuffs_align, buff_test, debuff_test, buffs[], buff_timers[], debuffs[], debuff_timers[], buff_txts[], } } #[derive(WidgetCommon)] pub struct BuffsBar<'a> { imgs: &'a Imgs, fonts: &'a Fonts, #[conrod(common_builder)] common: widget::CommonBuilder, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, localized_strings: &'a Localization, buffs: &'a Buffs, pulse: f32, global_state: &'a GlobalState, health: &'a Health, energy: &'a Energy, } impl<'a> BuffsBar<'a> { #[allow(clippy::too_many_arguments)] // TODO: Pending review in #587 pub fn new( imgs: &'a Imgs, fonts: &'a Fonts, rot_imgs: &'a ImgsRot, tooltip_manager: &'a mut TooltipManager, localized_strings: &'a Localization, buffs: &'a Buffs, pulse: f32, global_state: &'a GlobalState, health: &'a Health, energy: &'a Energy, ) -> Self { Self { imgs, fonts, common: widget::CommonBuilder::default(), rot_imgs, tooltip_manager, localized_strings, buffs, pulse, global_state, health, energy, } } } pub struct State { ids: Ids, } pub enum Event { RemoveBuff(BuffKind), } impl<'a> Widget for BuffsBar<'a> { type Event = Vec; type State = State; type Style = (); fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { State { ids: Ids::new(id_gen), } } fn style(&self) -> Self::Style {} fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; let mut event = Vec::new(); let localized_strings = self.localized_strings; let buffs = self.buffs; let buff_ani = ((self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8) + 0.5; //Animation timer let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani); let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0); let buff_position = self.global_state.settings.gameplay.buff_position; let buffs_tooltip = Tooltip::new({ // Edge images [t, b, r, l] // Corner images [tr, tl, br, bl] let edge = &self.rot_imgs.tt_side; let corner = &self.rot_imgs.tt_corner; ImageFrame::new( [edge.cw180, edge.none, edge.cw270, edge.cw90], [corner.none, corner.cw270, corner.cw90, corner.cw180], Color::Rgba(0.08, 0.07, 0.04, 1.0), 5.0, ) }) .title_font_size(self.fonts.cyri.scale(15)) .parent(ui.window) .desc_font_size(self.fonts.cyri.scale(12)) .font_id(self.fonts.cyri.conrod_id) .desc_text_color(TEXT_COLOR); if let BuffPosition::Bar = buff_position { let show_health = self.health.current() != self.health.maximum(); let show_stamina = self.energy.current() != self.energy.maximum(); let offset = if show_health && show_stamina { 140.0 } else if show_health || show_stamina { 95.0 } else { 55.0 }; // Alignment Rectangle::fill_with([484.0, 100.0], color::TRANSPARENT) .mid_bottom_with_margin_on(ui.window, offset) .set(state.ids.align, ui); Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT) .bottom_left_with_margins_on(state.ids.align, 0.0, 0.0) .set(state.ids.debuffs_align, ui); Rectangle::fill_with([484.0 / 2.0, 90.0], color::TRANSPARENT) .bottom_right_with_margins_on(state.ids.align, 0.0, 0.0) .set(state.ids.buffs_align, ui); // Buffs and Debuffs let (buff_count, debuff_count) = buffs.iter_active().map(hud::get_buff_info).fold( (0, 0), |(buff_count, debuff_count), info| { if info.is_buff { (buff_count + 1, debuff_count) } else { (buff_count, debuff_count + 1) } }, ); // Limit displayed buffs let buff_count = buff_count.min(12); let debuff_count = debuff_count.min(12); let gen = &mut ui.widget_id_generator(); if state.ids.buffs.len() < buff_count { state.update(|state| state.ids.buffs.resize(buff_count, gen)); }; if state.ids.debuffs.len() < debuff_count { state.update(|state| state.ids.debuffs.resize(debuff_count, gen)); }; if state.ids.buff_timers.len() < buff_count { state.update(|state| state.ids.buff_timers.resize(buff_count, gen)); }; if state.ids.debuff_timers.len() < debuff_count { state.update(|state| state.ids.debuff_timers.resize(debuff_count, gen)); }; // Create Buff Widgets let mut buff_vec = state .ids .buffs .iter() .copied() .zip(state.ids.buff_timers.iter().copied()) .zip( buffs .iter_active() .map(hud::get_buff_info) .filter(|info| info.is_buff), ) .collect::>(); // Sort the buffs by kind buff_vec.sort_by_key(|((_id, _timer_id), buff)| std::cmp::Reverse(buff.kind)); buff_vec .iter() .enumerate() .for_each(|(i, ((id, timer_id), buff))| { let max_duration = buff.data.duration; let current_duration = buff.dur; let duration_percentage = current_duration.map_or(1000.0, |cur| { max_duration .map_or(1000.0, |max| cur.as_secs_f32() / max.as_secs_f32() * 1000.0) }) as u32; // Percentage to determine which frame of the timer overlay is displayed let buff_img = hud::get_buff_image(buff.kind, self.imgs); let buff_widget = Image::new(buff_img).w_h(40.0, 40.0); // Sort buffs into rows of 11 slots let x = i % 6; let y = i / 6; let buff_widget = buff_widget.bottom_left_with_margins_on( state.ids.buffs_align, 0.0 + y as f64 * (41.0), 1.5 + x as f64 * (43.0), ); buff_widget .color( if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) { Some(pulsating_col) } else { Some(norm_col) }, ) .set(*id, ui); // Create Buff tooltip let title = hud::get_buff_title(buff.kind, localized_strings); let desc_txt = hud::get_buff_desc(buff.kind, localized_strings); let remaining_time = hud::get_buff_time(*buff); let click_to_remove = format!("<{}>", &localized_strings.get("buff.remove")); let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove); // Timer overlay if Button::image(match duration_percentage as u64 { 875..=1000 => self.imgs.nothing, // 8/8 750..=874 => self.imgs.buff_0, // 7/8 625..=749 => self.imgs.buff_1, // 6/8 500..=624 => self.imgs.buff_2, // 5/8 375..=499 => self.imgs.buff_3, // 4/8 250..=374 => self.imgs.buff_4, //3/8 125..=249 => self.imgs.buff_5, // 2/8 0..=124 => self.imgs.buff_6, // 1/8 _ => self.imgs.nothing, }) .w_h(40.0, 40.0) .middle_of(*id) .with_tooltip( self.tooltip_manager, title, &desc, &buffs_tooltip, BUFF_COLOR, ) .set(*timer_id, ui) .was_clicked() { event.push(Event::RemoveBuff(buff.kind)); }; }); // Create Debuff Widgets let mut debuff_vec = state .ids .debuffs .iter() .copied() .zip(state.ids.debuff_timers.iter().copied()) .zip( buffs .iter_active() .map(hud::get_buff_info) .filter(|info| !info.is_buff), ) .collect::>(); // Sort the debuffs by kind debuff_vec.sort_by_key(|((_id, _timer_id), debuff)| debuff.kind); debuff_vec .iter() .enumerate() .for_each(|(i, ((id, timer_id), debuff))| { let max_duration = debuff.data.duration; let current_duration = debuff.dur; let duration_percentage = current_duration.map_or(1000.0, |cur| { max_duration .map_or(1000.0, |max| cur.as_secs_f32() / max.as_secs_f32() * 1000.0) }) as u32; // Percentage to determine which frame of the timer overlay is displayed let debuff_img = match debuff.kind { BuffKind::Bleeding { .. } => self.imgs.debuff_bleed_0, BuffKind::Cursed { .. } => self.imgs.debuff_skull_0, _ => self.imgs.missing_icon, }; let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0); // Sort buffs into rows of 11 slots let x = i % 6; let y = i / 6; let debuff_widget = debuff_widget.bottom_right_with_margins_on( state.ids.debuffs_align, 0.0 + y as f64 * (41.0), 1.5 + x as f64 * (43.0), ); debuff_widget .color( if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) { Some(pulsating_col) } else { Some(norm_col) }, ) .set(*id, ui); // Create Debuff tooltip let title = hud::get_buff_title(debuff.kind, localized_strings); let desc_txt = hud::get_buff_desc(debuff.kind, localized_strings); let remaining_time = hud::get_buff_time(*debuff); let desc = format!("{}\n\n{}", desc_txt, remaining_time); Image::new(match duration_percentage as u64 { 875..=1000 => self.imgs.nothing, // 8/8 750..=874 => self.imgs.buff_0, // 7/8 625..=749 => self.imgs.buff_1, // 6/8 500..=624 => self.imgs.buff_2, // 5/8 375..=499 => self.imgs.buff_3, // 4/8 250..=374 => self.imgs.buff_4, //3/8 125..=249 => self.imgs.buff_5, // 2/8 0..=124 => self.imgs.buff_6, // 1/8 _ => self.imgs.nothing, }) .w_h(40.0, 40.0) .middle_of(*id) .with_tooltip( self.tooltip_manager, title, &desc, &buffs_tooltip, DEBUFF_COLOR, ) .set(*timer_id, ui); }); } if let BuffPosition::Map = buff_position { // Alignment Rectangle::fill_with([210.0, 210.0], color::TRANSPARENT) .top_right_with_margins_on(ui.window, 5.0, 270.0) .set(state.ids.align, ui); // Buffs and Debuffs let buff_count = buffs.kinds.len().min(11); // Limit displayed buffs let buff_count = buff_count.min(20); let gen = &mut ui.widget_id_generator(); if state.ids.buffs.len() < buff_count { state.update(|state| state.ids.buffs.resize(buff_count, gen)); }; if state.ids.buff_timers.len() < buff_count { state.update(|state| state.ids.buff_timers.resize(buff_count, gen)); }; if state.ids.buff_txts.len() < buff_count { state.update(|state| state.ids.buff_txts.resize(buff_count, gen)); }; // Create Buff Widgets let mut buff_vec = state .ids .buffs .iter() .copied() .zip(state.ids.buff_timers.iter().copied()) .zip(state.ids.buff_txts.iter().copied()) .zip(buffs.iter_active().map(hud::get_buff_info)) .collect::>(); // Sort the buffs by kind buff_vec.sort_by_key(|((_id, _timer_id), txt_id)| std::cmp::Reverse(txt_id.kind)); buff_vec .iter() .enumerate() .for_each(|(i, (((id, timer_id), txt_id), buff))| { let max_duration = buff.data.duration; let current_duration = buff.dur; // Percentage to determine which frame of the timer overlay is displayed let duration_percentage = current_duration.map_or(1000.0, |cur| { max_duration .map_or(1000.0, |max| cur.as_secs_f32() / max.as_secs_f32() * 1000.0) }) as u32; let buff_img = hud::get_buff_image(buff.kind, &self.imgs); let buff_widget = Image::new(buff_img).w_h(40.0, 40.0); // Sort buffs into rows of 6 slots let x = i % 6; let y = i / 6; let buff_widget = buff_widget.top_right_with_margins_on( state.ids.align, 0.0 + y as f64 * (54.0), 0.0 + x as f64 * (42.0), ); buff_widget .color( if current_duration.map_or(false, |cur| cur.as_secs_f32() < 10.0) { Some(pulsating_col) } else { Some(norm_col) }, ) .set(*id, ui); // Create Buff tooltip let title = hud::get_buff_title(buff.kind, localized_strings); let desc_txt = hud::get_buff_desc(buff.kind, localized_strings); let remaining_time = hud::get_buff_time(*buff); let click_to_remove = format!("<{}>", &localized_strings.get("buff.remove")); let desc = if buff.is_buff { format!("{}\n\n{}", desc_txt, click_to_remove) } else { desc_txt.to_string() }; // Timer overlay if Button::image(match duration_percentage as u64 { 875..=1000 => self.imgs.nothing, // 8/8 750..=874 => self.imgs.buff_0, // 7/8 625..=749 => self.imgs.buff_1, // 6/8 500..=624 => self.imgs.buff_2, // 5/8 375..=499 => self.imgs.buff_3, // 4/8 250..=374 => self.imgs.buff_4, // 3/8 125..=249 => self.imgs.buff_5, // 2/8 0..=124 => self.imgs.buff_6, // 1/8 _ => self.imgs.nothing, }) .w_h(40.0, 40.0) .middle_of(*id) .with_tooltip( self.tooltip_manager, title, &desc, &buffs_tooltip, if buff.is_buff { BUFF_COLOR } else { DEBUFF_COLOR }, ) .set(*timer_id, ui) .was_clicked() { event.push(Event::RemoveBuff(buff.kind)); } Text::new(&remaining_time) .down_from(*timer_id, 1.0) .font_size(self.fonts.cyri.scale(10)) .font_id(self.fonts.cyri.conrod_id) .graphics_for(*timer_id) .color(TEXT_COLOR) .set(*txt_id, ui); }); } event } }