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 = format!("<{}>", localized_strings.get_msg("buff-remove"));
287 let desc = format!("{}\n\n{}\n\n{}", desc_txt, remaining_time, click_to_remove);
288 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 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 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; let debuff_img = debuff.kind.image(self.imgs);
333 let debuff_widget = Image::new(debuff_img).w_h(40.0, 40.0);
334 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 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 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 let buff_count = buff_icons.len().min(11);
390 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 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 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 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 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 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 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, 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,
525 }
526 }
527}