1use super::{
2 DEFAULT_NPC, ENEMY_HP_COLOR, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR,
3 QUALITY_EPIC, REGION_COLOR, SAY_COLOR, STAMINA_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
4 cr_color, img_ids::Imgs,
5};
6use crate::{
7 GlobalState,
8 game_input::GameInput,
9 hud::{BuffIcon, IconHandler, controller_icons::LayerIconIds},
10 ui::{Ingameable, fonts::Fonts},
11 window::LastInput,
12};
13use common::{
14 comp::{Buffs, Energy, Health, SpeechBubble, SpeechBubbleType, Stance},
15 resources::Time,
16};
17use conrod_core::{
18 Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
19 position::Align,
20 widget::{self, Image, Rectangle, RoundedRectangle, Text},
21 widget_ids,
22};
23use i18n::Localization;
24
25const MAX_BUBBLE_WIDTH: f64 = 250.0;
26widget_ids! {
27 struct Ids {
28 speech_bubble_text,
30 speech_bubble_shadow,
31 speech_bubble_top_left,
32 speech_bubble_top,
33 speech_bubble_top_right,
34 speech_bubble_left,
35 speech_bubble_mid,
36 speech_bubble_right,
37 speech_bubble_bottom_left,
38 speech_bubble_bottom,
39 speech_bubble_bottom_right,
40 speech_bubble_tail,
41 speech_bubble_icon,
42
43 name_bg,
45 name,
46
47 level,
49 level_skull,
50 hardcore,
51 health_bar,
52 decay_bar,
53 health_bar_bg,
54 health_txt,
55 mana_bar,
56 health_bar_fg,
57
58 buffs_align,
60 buffs[],
61 buff_timers[],
62
63 interaction_hints,
65 interaction_hints_bg,
66 btns[], icns[], }
69}
70
71pub struct Info<'a> {
72 pub name: Option<String>,
73 pub health: Option<&'a Health>,
74 pub buffs: Option<&'a Buffs>,
75 pub energy: Option<&'a Energy>,
76 pub combat_rating: Option<f32>,
77 pub hardcore: bool,
78 pub stance: Option<&'a Stance>,
79}
80
81pub fn should_show_healthbar(health: &Health) -> bool {
83 (health.current() - health.maximum()).abs() > Health::HEALTH_EPSILON
84 || health.current() < health.base_max()
85}
86pub fn decayed_health_displayed(health: &Health) -> bool {
88 (1.0 - health.maximum() / health.base_max()) > 0.0
89}
90#[derive(WidgetCommon)]
93pub struct Overhead<'a> {
94 info: Option<Info<'a>>,
95 bubble: Option<&'a SpeechBubble>,
96 in_group: bool,
97 pulse: f32,
98 interaction_options: Vec<(GameInput, String)>,
99
100 i18n: &'a Localization,
101 imgs: &'a Imgs,
102 fonts: &'a Fonts,
103 time: &'a Time,
104 global_state: &'a GlobalState,
105
106 #[conrod(common_builder)]
107 common: widget::CommonBuilder,
108}
109
110impl<'a> Overhead<'a> {
111 pub fn new(
112 info: Option<Info<'a>>,
113 bubble: Option<&'a SpeechBubble>,
114 in_group: bool,
115 pulse: f32,
116 interaction_options: Vec<(GameInput, String)>,
117 i18n: &'a Localization,
118 imgs: &'a Imgs,
119 fonts: &'a Fonts,
120 time: &'a Time,
121 global_state: &'a GlobalState,
122 ) -> Self {
123 Self {
124 info,
125 bubble,
126 in_group,
127 pulse,
128 interaction_options,
129 i18n,
130 imgs,
131 fonts,
132 time,
133 global_state,
134 common: widget::CommonBuilder::default(),
135 }
136 }
137}
138
139pub struct State {
140 ids: Ids,
141}
142
143impl Ingameable for Overhead<'_> {
144 fn prim_count(&self) -> usize {
145 let info_ids = self.info.as_ref().map_or(0, |info| {
150 let mut count_ids = 2 + 1;
153
154 if self.bubble.is_none() {
159 let buff_ids = info
160 .buffs
161 .as_ref()
162 .map_or(0, |buffs| BuffIcon::icons_vec(buffs, info.stance).len());
163 count_ids += buff_ids.min(11) * 2;
164 }
165
166 if info.health.is_some_and(should_show_healthbar) {
175 count_ids += 5;
176 count_ids += info.energy.is_some() as usize;
177 count_ids += info.hardcore as usize;
178 }
179
180 count_ids += info.health.is_some_and(decayed_health_displayed) as usize;
182
183 if !self.interaction_options.is_empty() {
189 count_ids += match self.global_state.window.last_input() {
190 LastInput::KeyboardMouse => 2,
191 LastInput::Controller => 18,
192 };
193 }
194
195 count_ids
196 });
197
198 let bubble = if self.bubble.is_some() { 13 } else { 0 };
202
203 info_ids + bubble
204 }
205}
206
207impl Widget for Overhead<'_> {
208 type Event = ();
209 type State = State;
210 type Style = ();
211
212 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
213 State {
214 ids: Ids::new(id_gen),
215 }
216 }
217
218 fn style(&self) -> Self::Style {}
219
220 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
221 let widget::UpdateArgs { id, state, ui, .. } = args;
222 const BARSIZE: f64 = 2.0; const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
224 const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
225 if let Some(Info {
226 ref name,
227 health,
228 buffs,
229 energy,
230 combat_rating,
231 hardcore,
232 stance,
233 }) = self.info
234 {
235 let hp_percentage = health.map_or(100.0, |h| {
237 f64::from(h.current() / h.base_max().max(h.maximum()) * 100.0)
238 });
239 let health_current = health.map_or(1.0, |h| f64::from(h.current()));
241 let health_max = health.map_or(1.0, |h| f64::from(h.maximum()));
242 let name_y = if (health_current - health_max).abs() < 1e-6 {
243 MANA_BAR_Y + 20.0
244 } else {
245 MANA_BAR_Y + 32.0
246 };
247 let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 };
248 let health_cur_txt = if self.global_state.settings.interface.use_health_prefixes {
251 match health_current as u32 {
252 0..=999 => format!("{:.0}", health_current.max(1.0)),
253 1000..=999999 => format!("{:.0}K", (health_current / 1000.0).max(1.0)),
254 _ => format!("{:.0}M", (health_current / 1.0e6).max(1.0)),
255 }
256 } else {
257 format!("{:.0}", health_current.max(1.0))
258 };
259 let health_max_txt = if self.global_state.settings.interface.use_health_prefixes {
260 match health_max as u32 {
261 0..=999 => format!("{:.0}", health_max.max(1.0)),
262 1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)),
263 _ => format!("{:.0}M", (health_max / 1.0e6).max(1.0)),
264 }
265 } else {
266 format!("{:.0}", health_max.max(1.0))
267 };
268 let buff_icons = buffs
271 .as_ref()
272 .map(|buffs| BuffIcon::icons_vec(buffs, stance))
273 .unwrap_or_default();
274 let buff_count = buff_icons.len().min(11);
275 Rectangle::fill_with([168.0, 100.0], color::TRANSPARENT)
276 .x_y(-1.0, name_y + 60.0)
277 .parent(id)
278 .set(state.ids.buffs_align, ui);
279
280 let generator = &mut ui.widget_id_generator();
281 if state.ids.buffs.len() < buff_count {
282 state.update(|state| state.ids.buffs.resize(buff_count, generator));
283 };
284 if state.ids.buff_timers.len() < buff_count {
285 state.update(|state| state.ids.buff_timers.resize(buff_count, generator));
286 };
287
288 let buff_ani = ((self.pulse * 4.0).cos() * 0.5 + 0.8) + 0.5; let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
290 let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
291 if self.bubble.is_none() {
293 state
294 .ids
295 .buffs
296 .iter()
297 .copied()
298 .zip(state.ids.buff_timers.iter().copied())
299 .zip(buff_icons.iter())
300 .enumerate()
301 .for_each(|(i, ((id, timer_id), buff))| {
302 let max_duration = buff.kind.max_duration();
304 let current_duration = buff.end_time.map(|end| end - self.time.0);
305 let duration_percentage = current_duration.map_or(1000.0, |cur| {
306 max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
307 }) as u32; let buff_img = buff.kind.image(self.imgs);
309 let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
310 let x = i % 5;
312 let y = i / 5;
313 let buff_widget = buff_widget.bottom_left_with_margins_on(
314 state.ids.buffs_align,
315 0.0 + y as f64 * (21.0),
316 0.0 + x as f64 * (21.0),
317 );
318 buff_widget
319 .color(if current_duration.is_some_and(|cur| cur < 10.0) {
320 Some(pulsating_col)
321 } else {
322 Some(norm_col)
323 })
324 .set(id, ui);
325
326 Image::new(match duration_percentage as u64 {
327 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,
336 })
337 .w_h(20.0, 20.0)
338 .middle_of(id)
339 .set(timer_id, ui);
340 });
341 }
342 Text::new(name.as_deref().unwrap_or(""))
344 .font_id(self.fonts.cyri.conrod_id)
346 .font_size(font_size)
347 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
348 .x_y(-1.0, name_y)
349 .parent(id)
350 .set(state.ids.name_bg, ui);
351 Text::new(name.as_deref().unwrap_or(""))
352 .font_id(self.fonts.cyri.conrod_id)
354 .font_size(font_size)
355 .color(if self.in_group {
356 GROUP_MEMBER
357 } else {
360 DEFAULT_NPC
361 })
362 .x_y(0.0, name_y + 1.0)
363 .parent(id)
364 .set(state.ids.name, ui);
365
366 match health {
367 Some(health)
368 if should_show_healthbar(health) || decayed_health_displayed(health) =>
369 {
370 let hp_ani = (self.pulse * 4.0).cos() * 0.5 + 1.0; let crit_hp_color: Color = Color::Rgba(0.93, 0.59, 0.03, hp_ani);
373 let decayed_health = f64::from(1.0 - health.maximum() / health.base_max());
374 Image::new(if self.in_group {self.imgs.health_bar_group_bg} else {self.imgs.enemy_health_bg})
376 .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
377 .x_y(0.0, MANA_BAR_Y + 6.5) .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
379 .parent(id)
380 .set(state.ids.health_bar_bg, ui);
381
382 let size_factor = (hp_percentage / 100.0) * BARSIZE;
384 let w = if self.in_group {
385 82.0 * size_factor
386 } else {
387 73.0 * size_factor
388 };
389 let h = 6.0 * BARSIZE;
390 let x = if self.in_group {
391 (0.0 + (hp_percentage / 100.0 * 41.0 - 41.0)) * BARSIZE
392 } else {
393 (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE
394 };
395 Image::new(self.imgs.enemy_bar)
396 .w_h(w, h)
397 .x_y(x, MANA_BAR_Y + 8.0)
398 .color(if self.in_group {
399 Some(match hp_percentage {
401 x if (0.0..25.0).contains(&x) => crit_hp_color,
402 x if (25.0..50.0).contains(&x) => LOW_HP_COLOR,
403 _ => HP_COLOR,
404 })
405 } else {
406 Some(ENEMY_HP_COLOR)
407 })
408 .parent(id)
409 .set(state.ids.health_bar, ui);
410
411 if decayed_health > 0.0 {
412 let x_decayed = if self.in_group {
413 (0.0 - (decayed_health * 41.0 - 41.0)) * BARSIZE
414 } else {
415 (4.5 - (decayed_health * 36.45 - 36.45)) * BARSIZE
416 };
417
418 let decay_bar_len = decayed_health
419 * if self.in_group {
420 82.0 * BARSIZE
421 } else {
422 73.0 * BARSIZE
423 };
424 Image::new(self.imgs.enemy_bar)
425 .w_h(decay_bar_len, h)
426 .x_y(x_decayed, MANA_BAR_Y + 8.0)
427 .color(Some(QUALITY_EPIC))
428 .parent(id)
429 .set(state.ids.decay_bar, ui);
430 }
431 let mut txt = format!("{}/{}", health_cur_txt, health_max_txt);
432 if health.is_dead {
433 txt = self.i18n.get_msg("hud-group-dead").to_string()
434 };
435 Text::new(&txt)
436 .mid_top_with_margin_on(state.ids.health_bar_bg, 2.0)
437 .font_size(10)
438 .font_id(self.fonts.cyri.conrod_id)
439 .color(TEXT_COLOR)
440 .parent(id)
441 .set(state.ids.health_txt, ui);
442
443 if let Some(energy) = energy {
445 let energy_factor = f64::from(energy.current() / energy.maximum());
446 let size_factor = energy_factor * BARSIZE;
447 let w = if self.in_group {
448 80.0 * size_factor
449 } else {
450 72.0 * size_factor
451 };
452 let x = if self.in_group {
453 ((0.0 + (energy_factor * 40.0)) - 40.0) * BARSIZE
454 } else {
455 ((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE
456 };
457 Rectangle::fill_with([w, MANA_BAR_HEIGHT], STAMINA_COLOR)
458 .x_y(
459 x, MANA_BAR_Y, )
461 .parent(id)
462 .set(state.ids.mana_bar, ui);
463 }
464
465 Image::new(if self.in_group {self.imgs.health_bar_group} else {self.imgs.enemy_health})
467 .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
468 .x_y(0.0, MANA_BAR_Y + 6.5) .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
470 .parent(id)
471 .set(state.ids.health_bar_fg, ui);
472
473 if let Some(combat_rating) = combat_rating {
474 let indicator_col = cr_color(combat_rating);
475 let artifact_diffculty = 122.0;
476
477 if combat_rating > artifact_diffculty && !self.in_group {
478 let skull_ani =
479 ((self.pulse * 0.7).cos() * 0.5 + 0.5) * 10.0; Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
481 self.imgs.skull_2
482 } else {
483 self.imgs.skull
484 })
485 .w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
486 .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
487 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
488 .parent(id)
489 .set(state.ids.level_skull, ui);
490 } else {
491 Image::new(if self.in_group {
492 self.imgs.nothing
493 } else {
494 self.imgs.combat_rating_ico
495 })
496 .w_h(7.0 * BARSIZE, 7.0 * BARSIZE)
497 .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 6.0)
498 .color(Some(indicator_col))
499 .parent(id)
500 .set(state.ids.level, ui);
501 }
502 }
503
504 if hardcore {
505 Image::new(self.imgs.hardcore)
506 .w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
507 .x_y(39.0 * BARSIZE, MANA_BAR_Y + 13.0)
508 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
509 .parent(id)
510 .set(state.ids.hardcore, ui);
511 }
512 },
513 _ => {},
514 }
515
516 if !self.interaction_options.is_empty() {
518 let scale = 30.0;
519 let btn_rect_size = scale * 0.8;
520 let btn_font_size = scale * 0.6;
521 let btn_rect_pos_y;
522 let btn_radius = btn_rect_size / 5.0;
523 let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
524 let mut max_w = btn_rect_size;
525 let mut max_h = 0.0;
526 let mut box_offset = 0.0;
527
528 match self.global_state.window.last_input() {
529 LastInput::KeyboardMouse => {
530 let texts = self
531 .interaction_options
532 .iter()
533 .filter_map(|(input, action)| {
534 Some((
535 self.global_state.settings.controls.get_binding(*input)?,
536 action,
537 ))
538 })
539 .map(|(input, action)| {
540 format!("{} {}", input.display_string(), action)
541 })
542 .collect::<Vec<_>>()
543 .join("\n");
544
545 let hints_text = Text::new(&texts)
546 .font_id(self.fonts.cyri.conrod_id)
547 .font_size(btn_font_size as u32)
548 .color(TEXT_COLOR)
549 .parent(id)
550 .down_from(
551 self.info.map_or(state.ids.name, |info| {
552 if info.health.is_some_and(should_show_healthbar) {
553 if info.energy.is_some() {
554 state.ids.mana_bar
555 } else {
556 state.ids.health_bar
557 }
558 } else {
559 state.ids.name
560 }
561 }),
562 12.0,
563 )
564 .align_middle_x_of(state.ids.name)
565 .depth(1.0);
566
567 let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
568 max_w = max_w.max(w);
569 max_h += h;
570 hints_text.set(state.ids.interaction_hints, ui);
571 btn_rect_pos_y = 0.0;
572 },
573 LastInput::Controller => {
574 let max_controller_text = 4; if state.ids.btns.len() < max_controller_text {
585 state.update(|state| {
586 state
587 .ids
588 .btns
589 .resize(max_controller_text, &mut ui.widget_id_generator());
590 })
591 }
592
593 let icns_size = max_controller_text * 3; if state.ids.icns.len() < icns_size {
595 state.update(|state| {
596 state
597 .ids
598 .icns
599 .resize(icns_size, &mut ui.widget_id_generator());
600 })
601 }
602
603 let icon_handler = IconHandler::new(self.global_state, self.imgs);
604
605 let anchor_text = Text::new("")
607 .font_id(self.fonts.cyri.conrod_id)
608 .font_size(btn_font_size as u32)
609 .color(TEXT_COLOR)
610 .parent(id)
611 .down_from(
612 self.info.map_or(state.ids.name, |info| {
613 if info.health.is_some_and(should_show_healthbar) {
614 if info.energy.is_some() {
615 state.ids.mana_bar
616 } else {
617 state.ids.health_bar
618 }
619 } else {
620 state.ids.name
621 }
622 }),
623 12.0,
624 )
625 .align_middle_x_of(state.ids.name)
626 .depth(1.0);
627
628 anchor_text.set(state.ids.interaction_hints, ui);
629 let mut down_from_id = state.ids.interaction_hints;
630 let mut icons_w: u8 = 0;
631 let mut first_text_w = 0.0;
632
633 for i in 0..max_controller_text {
637 let text_id = state.ids.btns[i];
638 let idx_icns = i * 3;
639 let icon_ids = LayerIconIds {
640 main: state.ids.icns[idx_icns],
641 modifier1: state.ids.icns[idx_icns + 1],
642 modifier2: state.ids.icns[idx_icns + 2],
643 };
644
645 let row_data = self.interaction_options.get(i);
647 let action_text =
648 row_data.map(|(_, action)| action.as_str()).unwrap_or("");
649
650 let mut hints_text = Text::new(action_text)
652 .font_id(self.fonts.cyri.conrod_id)
653 .font_size(btn_font_size as u32)
654 .color(TEXT_COLOR)
655 .parent(id)
656 .depth(1.0);
657
658 if i == 0 {
659 hints_text = hints_text.middle_of(down_from_id);
661 } else {
662 hints_text = hints_text.down_from(down_from_id, 1.0);
664 }
665
666 if let Some((input, _)) = row_data {
668 let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
669 max_w = max_w.max(w);
670 max_h += h;
671
672 if i == 0 {
673 first_text_w = w;
674 }
675
676 hints_text.set(text_id, ui);
677 down_from_id = text_id;
678
679 let count = icon_handler.set_controller_icons_left(
680 *input, 17.0, text_id, &icon_ids, ui,
681 );
682 icons_w = icons_w.max(count);
683 } else {
684 hints_text.set(text_id, ui);
685 down_from_id = text_id;
686
687 icon_handler
689 .set_controller_icons_left_none(17.0, text_id, &icon_ids, ui);
690 }
691 }
692
693 let icon_largest_width = icons_w as f64 * 21.0;
694 let centroid_difference = (max_w / 2.0) - (first_text_w / 2.0);
695 let offset = icon_largest_width / 2.0;
696 box_offset = -(centroid_difference - offset);
697
698 max_w += icon_largest_width;
699 max_h = max_h.max(btn_rect_size);
700 btn_rect_pos_y = (max_h - btn_font_size + 2.0) / 2.0;
701 },
702 }
703
704 RoundedRectangle::fill_with(
705 [max_w + btn_radius * 2.0, max_h + btn_radius * 2.0],
706 btn_radius,
707 btn_color,
708 )
709 .depth(2.0)
710 .x_y_relative_to(
711 state.ids.interaction_hints,
712 0.0 - box_offset,
713 0.0 - btn_rect_pos_y,
714 )
715 .parent(id)
716 .set(state.ids.interaction_hints_bg, ui);
717 }
718 }
719 if let Some(bubble) = self.bubble {
721 let dark_mode = self.global_state.settings.interface.speech_bubble_dark_mode;
722 let bubble_contents: String = self.i18n.get_content(bubble.content());
723 let (text_color, shadow_color) = bubble_color(bubble, dark_mode);
724 let mut text = Text::new(&bubble_contents)
725 .color(text_color)
726 .font_id(self.fonts.cyri.conrod_id)
727 .font_size(18)
728 .up_from(state.ids.name, 26.0)
729 .x_align_to(state.ids.name, Align::Middle)
730 .parent(id);
731
732 if let Some(w) = text.get_w(ui)
733 && w > MAX_BUBBLE_WIDTH
734 {
735 text = text.w(MAX_BUBBLE_WIDTH);
736 }
737 Image::new(if dark_mode {
738 self.imgs.dark_bubble_top_left
739 } else {
740 self.imgs.speech_bubble_top_left
741 })
742 .w_h(16.0, 16.0)
743 .top_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
744 .parent(id)
745 .set(state.ids.speech_bubble_top_left, ui);
746 Image::new(if dark_mode {
747 self.imgs.dark_bubble_top
748 } else {
749 self.imgs.speech_bubble_top
750 })
751 .h(16.0)
752 .padded_w_of(state.ids.speech_bubble_text, -4.0)
753 .mid_top_with_margin_on(state.ids.speech_bubble_text, -20.0)
754 .parent(id)
755 .set(state.ids.speech_bubble_top, ui);
756 Image::new(if dark_mode {
757 self.imgs.dark_bubble_top_right
758 } else {
759 self.imgs.speech_bubble_top_right
760 })
761 .w_h(16.0, 16.0)
762 .top_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
763 .parent(id)
764 .set(state.ids.speech_bubble_top_right, ui);
765 Image::new(if dark_mode {
766 self.imgs.dark_bubble_left
767 } else {
768 self.imgs.speech_bubble_left
769 })
770 .w(16.0)
771 .padded_h_of(state.ids.speech_bubble_text, -4.0)
772 .mid_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
773 .parent(id)
774 .set(state.ids.speech_bubble_left, ui);
775 Image::new(if dark_mode {
776 self.imgs.dark_bubble_mid
777 } else {
778 self.imgs.speech_bubble_mid
779 })
780 .padded_wh_of(state.ids.speech_bubble_text, -4.0)
781 .top_left_with_margin_on(state.ids.speech_bubble_text, -4.0)
782 .parent(id)
783 .set(state.ids.speech_bubble_mid, ui);
784 Image::new(if dark_mode {
785 self.imgs.dark_bubble_right
786 } else {
787 self.imgs.speech_bubble_right
788 })
789 .w(16.0)
790 .padded_h_of(state.ids.speech_bubble_text, -4.0)
791 .mid_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
792 .parent(id)
793 .set(state.ids.speech_bubble_right, ui);
794 Image::new(if dark_mode {
795 self.imgs.dark_bubble_bottom_left
796 } else {
797 self.imgs.speech_bubble_bottom_left
798 })
799 .w_h(16.0, 16.0)
800 .bottom_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
801 .parent(id)
802 .set(state.ids.speech_bubble_bottom_left, ui);
803 Image::new(if dark_mode {
804 self.imgs.dark_bubble_bottom
805 } else {
806 self.imgs.speech_bubble_bottom
807 })
808 .h(16.0)
809 .padded_w_of(state.ids.speech_bubble_text, -4.0)
810 .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -20.0)
811 .parent(id)
812 .set(state.ids.speech_bubble_bottom, ui);
813 Image::new(if dark_mode {
814 self.imgs.dark_bubble_bottom_right
815 } else {
816 self.imgs.speech_bubble_bottom_right
817 })
818 .w_h(16.0, 16.0)
819 .bottom_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
820 .parent(id)
821 .set(state.ids.speech_bubble_bottom_right, ui);
822 let tail = Image::new(if dark_mode {
823 self.imgs.dark_bubble_tail
824 } else {
825 self.imgs.speech_bubble_tail
826 })
827 .parent(id)
828 .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -32.0);
829
830 if dark_mode {
831 tail.w_h(22.0, 13.0)
832 } else {
833 tail.w_h(22.0, 28.0)
834 }
835 .set(state.ids.speech_bubble_tail, ui);
836
837 let mut text_shadow = Text::new(&bubble_contents)
838 .color(shadow_color)
839 .font_id(self.fonts.cyri.conrod_id)
840 .font_size(18)
841 .x_relative_to(state.ids.speech_bubble_text, 1.0)
842 .y_relative_to(state.ids.speech_bubble_text, -1.0)
843 .parent(id);
844 text.depth(text_shadow.get_depth() - 1.0)
846 .set(state.ids.speech_bubble_text, ui);
847 if let Some(w) = text_shadow.get_w(ui)
848 && w > MAX_BUBBLE_WIDTH
849 {
850 text_shadow = text_shadow.w(MAX_BUBBLE_WIDTH);
851 }
852 text_shadow.set(state.ids.speech_bubble_shadow, ui);
853 let icon = if self.global_state.settings.interface.speech_bubble_icon {
854 bubble_icon(bubble, self.imgs)
855 } else {
856 self.imgs.nothing
857 };
858 Image::new(icon)
859 .w_h(16.0, 16.0)
860 .top_left_with_margin_on(state.ids.speech_bubble_text, -16.0)
861 .set(state.ids.speech_bubble_icon, ui);
864 }
865 }
866}
867
868fn bubble_color(bubble: &SpeechBubble, dark_mode: bool) -> (Color, Color) {
869 let light_color = match bubble.icon {
870 SpeechBubbleType::Tell => TELL_COLOR,
871 SpeechBubbleType::Say => SAY_COLOR,
872 SpeechBubbleType::Region => REGION_COLOR,
873 SpeechBubbleType::Group => GROUP_COLOR,
874 SpeechBubbleType::Faction => FACTION_COLOR,
875 SpeechBubbleType::World
876 | SpeechBubbleType::Quest
877 | SpeechBubbleType::Trade
878 | SpeechBubbleType::None => TEXT_COLOR,
879 };
880 if dark_mode {
881 (light_color, TEXT_BG)
882 } else {
883 (TEXT_BG, light_color)
884 }
885}
886
887fn bubble_icon(sb: &SpeechBubble, imgs: &Imgs) -> conrod_core::image::Id {
888 match sb.icon {
889 SpeechBubbleType::Tell => imgs.chat_tell_small,
891 SpeechBubbleType::Say => imgs.chat_say_small,
892 SpeechBubbleType::Region => imgs.chat_region_small,
893 SpeechBubbleType::Group => imgs.chat_group_small,
894 SpeechBubbleType::Faction => imgs.chat_faction_small,
895 SpeechBubbleType::World => imgs.chat_world_small,
896 SpeechBubbleType::Quest => imgs.nothing, SpeechBubbleType::Trade => imgs.nothing, SpeechBubbleType::None => imgs.nothing, }
900}