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