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; 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 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 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 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 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 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 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; let buff_img = buff.kind.image(self.imgs);
252 let buff_widget = Image::new(buff_img).w_h(40.0, 40.0);
253 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 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 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 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 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; let debuff_img = debuff.kind.image(self.imgs);
334 let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0);
335 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 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 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 let buff_count = buff_icons.len().min(11);
391 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 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 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 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 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 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 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, 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,
527 }
528 }
529}