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; 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 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 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 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 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 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 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; let buff_img = buff.kind.image(self.imgs);
234 let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
235 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 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 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 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 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; let debuff_img = debuff.kind.image(self.imgs);
316 let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0);
317 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 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 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 let buff_count = buff_icons.len().min(11);
373 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 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 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 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 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 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 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, 750..=874 => self.imgs.buff_0, 625..=749 => self.imgs.buff_1, 500..=624 => self.imgs.buff_2, 375..=499 => self.imgs.buff_3, 250..=374 => self.imgs.buff_4, 125..=249 => self.imgs.buff_5, 0..=124 => self.imgs.buff_6, _ => self.imgs.nothing,
504 }
505 }
506}