veloren_voxygen/hud/
group.rs

1use super::{
2    BLACK, BUFF_COLOR, DEBUFF_COLOR, ERROR_COLOR, GROUP_COLOR, HP_COLOR, KILL_COLOR, LOW_HP_COLOR,
3    QUALITY_EPIC, STAMINA_COLOR, Show, TEXT_COLOR, TEXT_COLOR_GREY, UI_HIGHLIGHT_0, UI_MAIN,
4    cr_color,
5    img_ids::{Imgs, ImgsRot},
6};
7
8use crate::{
9    GlobalState,
10    game_input::GameInput,
11    hud::BuffIcon,
12    settings::Settings,
13    ui::{ImageFrame, Tooltip, TooltipManager, Tooltipable, fonts::Fonts},
14};
15use client::{self, Client};
16use common::{
17    combat,
18    comp::{Stats, group::Role, inventory::item::MaterialStatManifest, invite::InviteKind},
19    resources::Time,
20    uid::{IdMaps, Uid},
21};
22use common_net::sync::WorldSyncExt;
23use conrod_core::{
24    Color, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, color,
25    position::{Place, Relative},
26    widget::{self, Button, Image, Rectangle, Scrollbar, Text},
27    widget_ids,
28};
29use i18n::Localization;
30use specs::WorldExt;
31
32widget_ids! {
33    pub struct Ids {
34        group_button,
35        bg,
36        title,
37        title_bg,
38        btn_bg,
39        btn_friend,
40        btn_leader,
41        btn_link,
42        btn_kick,
43        btn_leave,
44        scroll_area,
45        scrollbar,
46        members[],
47        bubble_frame,
48        btn_accept,
49        btn_decline,
50        member_panels_bg[],
51        member_panels_frame[],
52        member_panels_txt_bg[],
53        member_panels_txt[],
54        member_health[],
55        member_health_decay[],
56        member_energy[],
57        buffs[],
58        buff_timers[],
59        dead_txt[],
60        health_txt[],
61        combat_rating_indicators[],
62        hardcore_indicators[],
63        timeout_bg,
64        timeout,
65    }
66}
67
68pub struct State {
69    ids: Ids,
70    // Selected group member
71    selected_member: Option<Uid>,
72}
73
74#[derive(WidgetCommon)]
75pub struct Group<'a> {
76    show: &'a mut Show,
77    client: &'a Client,
78    settings: &'a Settings,
79    imgs: &'a Imgs,
80    rot_imgs: &'a ImgsRot,
81    fonts: &'a Fonts,
82    localized_strings: &'a Localization,
83    pulse: f32,
84    global_state: &'a GlobalState,
85    tooltip_manager: &'a mut TooltipManager,
86    msm: &'a MaterialStatManifest,
87    time: &'a Time,
88
89    #[conrod(common_builder)]
90    common: widget::CommonBuilder,
91}
92
93impl<'a> Group<'a> {
94    pub fn new(
95        show: &'a mut Show,
96        client: &'a Client,
97        settings: &'a Settings,
98        imgs: &'a Imgs,
99        rot_imgs: &'a ImgsRot,
100        fonts: &'a Fonts,
101        localized_strings: &'a Localization,
102        pulse: f32,
103        global_state: &'a GlobalState,
104        tooltip_manager: &'a mut TooltipManager,
105        msm: &'a MaterialStatManifest,
106        time: &'a Time,
107    ) -> Self {
108        Self {
109            show,
110            client,
111            settings,
112            imgs,
113            rot_imgs,
114            fonts,
115            localized_strings,
116            pulse,
117            global_state,
118            tooltip_manager,
119            msm,
120            time,
121            common: widget::CommonBuilder::default(),
122        }
123    }
124}
125
126pub enum Event {
127    Accept,
128    Decline,
129    Kick(Uid),
130    LeaveGroup,
131    AssignLeader(Uid),
132}
133
134impl Widget for Group<'_> {
135    type Event = Vec<Event>;
136    type State = State;
137    type Style = ();
138
139    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
140        Self::State {
141            ids: Ids::new(id_gen),
142            selected_member: None,
143        }
144    }
145
146    fn style(&self) -> Self::Style {}
147
148    //TODO: Disband groups when there's only one member in them
149    //TODO: Always send health, energy, level and position of group members to the
150    // client
151    fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
152        common_base::prof_span!("Group::update");
153        let widget::UpdateArgs { state, ui, .. } = args;
154        let mut events = Vec::new();
155        let localized_strings = self.localized_strings;
156        let buff_ani = ((self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8) + 0.5; //Animation timer
157        let debug_on = self.global_state.settings.interface.toggle_debug;
158        let offset = if debug_on { 270.0 } else { 0.0 };
159        let buffs_tooltip = Tooltip::new({
160            // Edge images [t, b, r, l]
161            // Corner images [tr, tl, br, bl]
162            let edge = &self.rot_imgs.tt_side;
163            let corner = &self.rot_imgs.tt_corner;
164            ImageFrame::new(
165                [edge.cw180, edge.none, edge.cw270, edge.cw90],
166                [corner.none, corner.cw270, corner.cw90, corner.cw180],
167                Color::Rgba(0.08, 0.07, 0.04, 1.0),
168                5.0,
169            )
170        })
171        .title_font_size(self.fonts.cyri.scale(15))
172        .parent(ui.window)
173        .desc_font_size(self.fonts.cyri.scale(12))
174        .font_id(self.fonts.cyri.conrod_id)
175        .desc_text_color(TEXT_COLOR);
176
177        // Don't show pets
178        let group_members = self
179            .client
180            .group_members()
181            .iter()
182            .filter_map(|(u, r)| match r {
183                Role::Member => Some(u),
184                Role::Pet => None,
185            })
186            .collect::<Vec<_>>();
187        // Not considered in group for ui purposes if it is just pets
188        let in_group = !group_members.is_empty();
189        if !in_group {
190            self.show.group_menu = false;
191            self.show.group = false;
192        }
193
194        // Helper
195        let uid_to_name_text = |uid: Uid, client: &Client| match client.player_list().get(&uid) {
196            Some(player_info) => player_info.character.as_ref().map_or_else(
197                || format!("Player<{}>", uid),
198                |c| self.localized_strings.get_content(&c.name),
199            ),
200            None => client
201                .state()
202                .ecs()
203                .entity_from_uid(uid)
204                .and_then(|entity| {
205                    client
206                        .state()
207                        .ecs()
208                        .read_storage::<Stats>()
209                        .get(entity)
210                        .map(|stats| self.localized_strings.get_content(&stats.name))
211                })
212                .unwrap_or_else(|| format!("Npc<{}>", uid)),
213        };
214
215        let open_invite = self.client.invite().and_then(|invite| {
216            // Don't show invite if it comes from a muted player
217            if self
218                .client
219                .player_list()
220                .get(&invite.0)
221                .is_none_or(|player| {
222                    self.global_state
223                        .profile
224                        .mutelist
225                        .contains_key(&player.uuid)
226                })
227            {
228                None
229            } else {
230                Some(invite)
231            }
232        });
233
234        let my_uid = self.client.uid();
235
236        // TODO show something to the player when they click on the group button while
237        // they are not in a group so that it doesn't look like the button is
238        // broken
239        if self.show.group_menu || open_invite.is_some() {
240            // Frame
241            Rectangle::fill_with([220.0, 140.0], Color::Rgba(0.0, 0.0, 0.0, 0.8))
242                .bottom_left_with_margins_on(ui.window, 108.0, 490.0)
243                .crop_kids()
244                .set(state.ids.bg, ui);
245        }
246        if let Some((_, timeout_start, timeout_dur, _)) = open_invite {
247            // Group Menu button
248            Button::image(self.imgs.group_icon)
249                .w_h(49.0, 26.0)
250                .bottom_left_with_margins_on(ui.window, 10.0, 490.0)
251                .set(state.ids.group_button, ui);
252            // Show timeout bar
253            let timeout_progress =
254                1.0 - timeout_start.elapsed().as_secs_f32() / timeout_dur.as_secs_f32();
255            Image::new(self.imgs.progress_frame)
256                .w_h(100.0, 10.0)
257                .middle_of(state.ids.bg)
258                .color(Some(UI_MAIN))
259                .set(state.ids.timeout_bg, ui);
260            Image::new(self.imgs.progress)
261                .w_h(98.0 * timeout_progress as f64, 8.0)
262                .top_left_with_margins_on(state.ids.timeout_bg, 1.0, 1.0)
263                .color(Some(UI_HIGHLIGHT_0))
264                .set(state.ids.timeout, ui);
265        }
266        // Buttons
267        if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) {
268            // Group Menu Button
269            if Button::image(if self.show.group_menu {
270                self.imgs.group_icon_press
271            } else {
272                self.imgs.group_icon
273            })
274            .w_h(49.0, 26.0)
275            .bottom_left_with_margins_on(ui.window, 10.0, 490.0)
276            .hover_image(self.imgs.group_icon_hover)
277            .press_image(self.imgs.group_icon_press)
278            .set(state.ids.group_button, ui)
279            .was_clicked()
280            {
281                self.show.group_menu = !self.show.group_menu;
282            };
283            Text::new(&group_name)
284                .up_from(state.ids.group_button, 5.0)
285                .font_size(14)
286                .font_id(self.fonts.cyri.conrod_id)
287                .color(BLACK)
288                .set(state.ids.title_bg, ui);
289            Text::new(&group_name)
290                .bottom_right_with_margins_on(state.ids.title_bg, 1.0, 1.0)
291                .font_size(14)
292                .font_id(self.fonts.cyri.conrod_id)
293                .color(TEXT_COLOR)
294                .set(state.ids.title, ui);
295            // Member panels
296            let group_size = group_members.len();
297            if state.ids.member_panels_bg.len() < group_size {
298                state.update(|s| {
299                    s.ids
300                        .member_panels_bg
301                        .resize(group_size, &mut ui.widget_id_generator())
302                })
303            };
304            if state.ids.member_health.len() < group_size {
305                state.update(|s| {
306                    s.ids
307                        .member_health
308                        .resize(group_size, &mut ui.widget_id_generator());
309                })
310            };
311            if state.ids.member_health_decay.len() < group_size {
312                state.update(|s| {
313                    s.ids
314                        .member_health_decay
315                        .resize(group_size, &mut ui.widget_id_generator());
316                })
317            };
318            if state.ids.member_energy.len() < group_size {
319                state.update(|s| {
320                    s.ids
321                        .member_energy
322                        .resize(group_size, &mut ui.widget_id_generator())
323                })
324            };
325            if state.ids.member_panels_frame.len() < group_size {
326                state.update(|s| {
327                    s.ids
328                        .member_panels_frame
329                        .resize(group_size, &mut ui.widget_id_generator())
330                })
331            };
332            if state.ids.member_panels_txt.len() < group_size {
333                state.update(|s| {
334                    s.ids
335                        .member_panels_txt
336                        .resize(group_size, &mut ui.widget_id_generator())
337                })
338            };
339            if state.ids.dead_txt.len() < group_size {
340                state.update(|s| {
341                    s.ids
342                        .dead_txt
343                        .resize(group_size, &mut ui.widget_id_generator())
344                })
345            };
346            if state.ids.health_txt.len() < group_size {
347                state.update(|s| {
348                    s.ids
349                        .health_txt
350                        .resize(group_size, &mut ui.widget_id_generator())
351                })
352            };
353            if state.ids.member_panels_txt_bg.len() < group_size {
354                state.update(|s| {
355                    s.ids
356                        .member_panels_txt_bg
357                        .resize(group_size, &mut ui.widget_id_generator())
358                })
359            };
360            if state.ids.combat_rating_indicators.len() < group_size {
361                state.update(|s| {
362                    s.ids
363                        .combat_rating_indicators
364                        .resize(group_size, &mut ui.widget_id_generator())
365                })
366            };
367            if state.ids.hardcore_indicators.len() < group_size {
368                state.update(|s| {
369                    s.ids
370                        .hardcore_indicators
371                        .resize(group_size, &mut ui.widget_id_generator())
372                })
373            };
374            let client_state = self.client.state();
375            let stats = client_state.ecs().read_storage::<Stats>();
376            let skill_sets = client_state.ecs().read_storage::<common::comp::SkillSet>();
377            let healths = client_state.ecs().read_storage::<common::comp::Health>();
378            let energy = client_state.ecs().read_storage::<common::comp::Energy>();
379            let buffs = client_state.ecs().read_storage::<common::comp::Buffs>();
380            let inventory = client_state.ecs().read_storage::<common::comp::Inventory>();
381            let id_maps = client_state.ecs().read_resource::<IdMaps>();
382            let bodies = client_state.ecs().read_storage::<common::comp::Body>();
383            let poises = client_state.ecs().read_storage::<common::comp::Poise>();
384            let stances = client_state.ecs().read_storage::<common::comp::Stance>();
385            let hardcore = client_state.ecs().read_storage::<common::comp::Hardcore>();
386
387            // Keep track of the total number of widget ids we are using for buffs
388            let mut total_buff_count = 0;
389            for (i, &uid) in group_members.iter().copied().enumerate() {
390                self.show.group = true;
391                let entity = id_maps.uid_entity(uid);
392                let stats = entity.and_then(|entity| stats.get(entity));
393                let skill_set = entity.and_then(|entity| skill_sets.get(entity));
394                let health = entity.and_then(|entity| healths.get(entity));
395                let energy = entity.and_then(|entity| energy.get(entity));
396                let buffs = entity.and_then(|entity| buffs.get(entity));
397                let inventory = entity.and_then(|entity| inventory.get(entity));
398                let is_leader = uid == leader;
399                let body = entity.and_then(|entity| bodies.get(entity));
400                let poise = entity.and_then(|entity| poises.get(entity));
401                let stance = entity.and_then(|entity| stances.get(entity));
402                let hardcore = entity.and_then(|entity| hardcore.get(entity));
403
404                if let (
405                    Some(stats),
406                    Some(skill_set),
407                    Some(inventory),
408                    Some(health),
409                    Some(energy),
410                    Some(body),
411                    Some(poise),
412                ) = (stats, skill_set, inventory, health, energy, body, poise)
413                {
414                    let combat_rating = combat::combat_rating(
415                        inventory, health, energy, poise, skill_set, *body, self.msm,
416                    );
417                    let char_name = self.localized_strings.get_content(&stats.name);
418                    let health_perc = health.current() / health.base_max().max(health.maximum());
419                    // change panel positions when debug info is shown
420                    let x = if debug_on { i / 8 } else { i / 11 };
421                    let y = if debug_on { i % 8 } else { i % 11 };
422                    let back = Image::new(self.imgs.member_bg).top_left_with_margins_on(
423                        ui.window,
424                        50.0 + offset + y as f64 * 77.0,
425                        10.0 + x as f64 * 180.0,
426                    );
427                    let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
428                    let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
429                    let health_col = match (health_perc * 100.0) as u8 {
430                        0..=20 => crit_hp_color,
431                        21..=40 => LOW_HP_COLOR,
432                        _ => HP_COLOR,
433                    };
434                    // Don't show panel for the player!
435                    // Panel BG
436                    back.w_h(152.0, 36.0)
437                        .color(if is_leader {
438                            Some(ERROR_COLOR)
439                        } else {
440                            Some(TEXT_COLOR)
441                        })
442                        .set(state.ids.member_panels_bg[i], ui);
443                    // Health
444                    Image::new(self.imgs.bar_content)
445                        .w_h(148.0 * f64::from(health_perc), 22.0)
446                        .color(Some(health_col))
447                        .top_left_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0)
448                        .set(state.ids.member_health[i], ui);
449                    // Health Decay
450                    let decayed_health = f64::from(1.0 - health.maximum() / health.base_max());
451                    if decayed_health > 0.0 {
452                        let decay_bar_len = 148.0 * decayed_health;
453                        Image::new(self.imgs.bar_content)
454                            .w_h(decay_bar_len, 22.0)
455                            .color(Some(QUALITY_EPIC))
456                            .top_right_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0)
457                            .set(state.ids.member_health_decay[i], ui);
458                    }
459                    if health.is_dead {
460                        // Death Text
461                        Text::new(&self.localized_strings.get_msg("hud-group-dead"))
462                            .mid_top_with_margin_on(state.ids.member_panels_bg[i], 1.0)
463                            .font_size(20)
464                            .font_id(self.fonts.cyri.conrod_id)
465                            .color(KILL_COLOR)
466                            .set(state.ids.dead_txt[i], ui);
467                    } else {
468                        // Health Text
469                        let txt = format!(
470                            "{}/{}",
471                            health.current().round() as u32,
472                            health.maximum().round() as u32,
473                        );
474                        // Change font size depending on health amount
475                        let font_size = match health.maximum() {
476                            x if (0.0..100.0).contains(&x) => 14,
477                            x if (100.0..=1000.0).contains(&x) => 13,
478                            x if (1000.0..=10000.0).contains(&x) => 12,
479                            _ => 11,
480                        };
481                        // Change text offset depending on health amount
482                        let txt_offset = match health.maximum() {
483                            x if (0.0..=100.0).contains(&x) => 4.0,
484                            x if (100.0..=1000.0).contains(&x) => 4.5,
485                            x if (1000.0..=10000.0).contains(&x) => 5.0,
486                            _ => 5.5,
487                        };
488                        Text::new(&txt)
489                            .mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset)
490                            .font_size(font_size)
491                            .font_id(self.fonts.cyri.conrod_id)
492                            .color(Color::Rgba(1.0, 1.0, 1.0, 0.5))
493                            .set(state.ids.health_txt[i], ui);
494                    };
495
496                    // Panel Frame
497                    Image::new(self.imgs.member_frame)
498                        .w_h(152.0, 36.0)
499                        .middle_of(state.ids.member_panels_bg[i])
500                        .color(Some(UI_HIGHLIGHT_0))
501                        .set(state.ids.member_panels_frame[i], ui);
502
503                    let indicator_col = cr_color(combat_rating);
504                    Image::new(self.imgs.combat_rating_ico_shadow)
505                        .w_h(18.0, 18.0)
506                        .top_left_with_margins_on(state.ids.member_panels_frame[i], -20.0, 2.0)
507                        .color(Some(indicator_col))
508                        .set(state.ids.combat_rating_indicators[i], ui);
509                    if hardcore.is_some() {
510                        Image::new(self.imgs.hardcore)
511                            .w_h(18.0, 18.0)
512                            .top_left_with_margins_on(state.ids.member_panels_frame[i], -20.0, 22.0)
513                            .set(state.ids.hardcore_indicators[i], ui);
514                    }
515                    // Panel Text
516                    Text::new(&char_name)
517                     .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 22.0 + hardcore.map_or(0.0, |_| 20.0))
518                     .font_size(20)
519                     .font_id(self.fonts.cyri.conrod_id)
520                     .color(BLACK)
521                     .w(300.0) // limit name length display
522                     .set(state.ids.member_panels_txt_bg[i], ui);
523                    Text::new(&char_name)
524                            .bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0)
525                            .font_size(20)
526                            .font_id(self.fonts.cyri.conrod_id)
527                            .color(if is_leader { ERROR_COLOR } else { GROUP_COLOR })
528                            .w(300.0) // limit name length display
529                            .set(state.ids.member_panels_txt[i], ui);
530                    let stam_perc = energy.current() / energy.maximum();
531                    // Energy
532                    Image::new(self.imgs.bar_content)
533                        .w_h(100.0 * f64::from(stam_perc), 8.0)
534                        .color(Some(STAMINA_COLOR))
535                        .top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
536                        .set(state.ids.member_energy[i], ui);
537                    if let Some(buffs) = buffs {
538                        let buff_icons = BuffIcon::icons_vec(buffs, stance);
539                        // Limit displayed buffs to 11
540                        let buff_count = buff_icons.len().min(11);
541                        total_buff_count += buff_count;
542                        let gen = &mut ui.widget_id_generator();
543                        if state.ids.buffs.len() < total_buff_count {
544                            state.update(|state| state.ids.buffs.resize(total_buff_count, gen));
545                        }
546                        if state.ids.buff_timers.len() < total_buff_count {
547                            state.update(|state| {
548                                state.ids.buff_timers.resize(total_buff_count, gen)
549                            });
550                        }
551                        // Create Buff Widgets
552                        let mut prev_id = None;
553                        state
554                            .ids
555                            .buffs
556                            .iter()
557                            .copied()
558                            .zip(state.ids.buff_timers.iter().copied())
559                            .skip(total_buff_count - buff_count)
560                            .zip(buff_icons.iter())
561                            .for_each(|((id, timer_id), buff)| {
562                                let max_duration = buff.kind.max_duration();
563                                let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
564                                let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
565                                let current_duration = buff.end_time.map(|end| end - self.time.0);
566                                let duration_percentage = current_duration.map_or(1000.0, |cur| {
567                                    max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
568                                }) as u32; // Percentage to determine which frame of the timer overlay is displayed
569                                let buff_img = buff.kind.image(self.imgs);
570                                let buff_widget = Image::new(buff_img).w_h(15.0, 15.0);
571                                let buff_widget = if let Some(id) = prev_id {
572                                    buff_widget.right_from(id, 1.0)
573                                } else {
574                                    buff_widget.bottom_left_with_margins_on(
575                                        state.ids.member_panels_frame[i],
576                                        -16.0,
577                                        1.0,
578                                    )
579                                };
580                                prev_id = Some(id);
581                                buff_widget
582                                    .color(if current_duration.is_some_and(|cur| cur < 10.0) {
583                                        Some(pulsating_col)
584                                    } else {
585                                        Some(norm_col)
586                                    })
587                                    .set(id, ui);
588                                // Create Buff tooltip
589                                let (title, desc_txt) =
590                                    buff.kind.title_description(localized_strings);
591                                let remaining_time = buff.get_buff_time(*self.time);
592                                let desc = format!("{}\n\n{}", desc_txt, remaining_time);
593                                Image::new(match duration_percentage as u64 {
594                                    875..=1000 => self.imgs.nothing, // 8/8
595                                    750..=874 => self.imgs.buff_0,   // 7/8
596                                    625..=749 => self.imgs.buff_1,   // 6/8
597                                    500..=624 => self.imgs.buff_2,   // 5/8
598                                    375..=499 => self.imgs.buff_3,   // 4/8
599                                    250..=374 => self.imgs.buff_4,   // 3/8
600                                    125..=249 => self.imgs.buff_5,   // 2/8
601                                    0..=124 => self.imgs.buff_6,     // 1/8
602                                    _ => self.imgs.nothing,
603                                })
604                                .w_h(15.0, 15.0)
605                                .middle_of(id)
606                                .with_tooltip(
607                                    self.tooltip_manager,
608                                    &title,
609                                    &desc,
610                                    &buffs_tooltip,
611                                    if buff.is_buff {
612                                        BUFF_COLOR
613                                    } else {
614                                        DEBUFF_COLOR
615                                    },
616                                )
617                                .set(timer_id, ui);
618                            });
619                    } else {
620                        // Values N.A.
621                        Text::new(&self.localized_strings.get_content(&stats.name))
622                            .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
623                            .font_size(20)
624                            .font_id(self.fonts.cyri.conrod_id)
625                            .color(GROUP_COLOR)
626                            .set(state.ids.member_panels_txt[i], ui);
627                        let back = if i == 0 {
628                            Image::new(self.imgs.member_bg)
629                                .top_left_with_margins_on(ui.window, offset, 20.0)
630                        } else {
631                            Image::new(self.imgs.member_bg)
632                                .down_from(state.ids.member_panels_bg[i - 1], 40.0)
633                        };
634                        back.w_h(152.0, 36.0)
635                            .color(Some(TEXT_COLOR))
636                            .set(state.ids.member_panels_bg[i], ui);
637                        // Panel Frame
638                        Image::new(self.imgs.member_frame)
639                            .w_h(152.0, 36.0)
640                            .middle_of(state.ids.member_panels_bg[i])
641                            .color(Some(UI_HIGHLIGHT_0))
642                            .set(state.ids.member_panels_frame[i], ui);
643                        // Panel Text
644                        Text::new(&self.localized_strings.get_msg("hud-group-out_of_range"))
645                            .mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0)
646                            .font_size(16)
647                            .font_id(self.fonts.cyri.conrod_id)
648                            .color(TEXT_COLOR)
649                            .set(state.ids.dead_txt[i], ui);
650                    }
651                }
652            }
653
654            if self.show.group_menu {
655                let selected = state.selected_member;
656                if Button::image(self.imgs.button) // Change button behaviour and style when the friendslist is working
657                    .w_h(90.0, 22.0)
658                    .top_right_with_margins_on(state.ids.bg, 5.0, 5.0)
659                    .hover_image(self.imgs.button)
660                    .press_image(self.imgs.button)
661                    .label_color(TEXT_COLOR_GREY)
662                    .image_color(TEXT_COLOR_GREY)
663                    .label(&self.localized_strings.get_msg("hud-group-add_friend"))
664                    .label_font_id(self.fonts.cyri.conrod_id)
665                    .label_font_size(self.fonts.cyri.scale(10))
666                    .set(state.ids.btn_friend, ui)
667                    .was_clicked()
668                {};
669                if Button::image(self.imgs.button)
670                    .w_h(90.0, 22.0)
671                    .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0)
672                    .hover_image(self.imgs.button_hover)
673                    .press_image(self.imgs.button_press)
674                    .label(&self.localized_strings.get_msg("hud-group-leave"))
675                    .label_color(TEXT_COLOR)
676                    .label_font_id(self.fonts.cyri.conrod_id)
677                    .label_font_size(self.fonts.cyri.scale(10))
678                    .set(state.ids.btn_leave, ui)
679                    .was_clicked()
680                {
681                    self.show.group_menu = false;
682                    self.show.group = !self.show.group;
683                    events.push(Event::LeaveGroup);
684                };
685                // Group leader functions
686                if my_uid == Some(leader) {
687                    if Button::image(self.imgs.button)
688                        .w_h(90.0, 22.0)
689                        .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0)
690                        .hover_image(self.imgs.button_hover)
691                        .press_image(self.imgs.button_press)
692                        .label(&self.localized_strings.get_msg("hud-group-assign_leader"))
693                        .label_color(if state.selected_member.is_some() {
694                            TEXT_COLOR
695                        } else {
696                            TEXT_COLOR_GREY
697                        })
698                        .label_font_id(self.fonts.cyri.conrod_id)
699                        .label_font_size(self.fonts.cyri.scale(10))
700                        .set(state.ids.btn_leader, ui)
701                        .was_clicked()
702                    {
703                        if let Some(uid) = selected {
704                            events.push(Event::AssignLeader(uid));
705                            state.update(|s| {
706                                s.selected_member = None;
707                            });
708                        }
709                    };
710                    if Button::image(self.imgs.button)
711                        .w_h(90.0, 22.0)
712                        .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0)
713                        .hover_image(self.imgs.button)
714                        .press_image(self.imgs.button)
715                        .label(&self.localized_strings.get_msg("hud-group-link_group"))
716                        .hover_image(self.imgs.button)
717                        .press_image(self.imgs.button)
718                        .label_color(TEXT_COLOR_GREY)
719                        .image_color(TEXT_COLOR_GREY)
720                        .label_font_id(self.fonts.cyri.conrod_id)
721                        .label_font_size(self.fonts.cyri.scale(10))
722                        .set(state.ids.btn_link, ui)
723                        .was_clicked()
724                    {};
725                    if Button::image(self.imgs.button)
726                        .w_h(90.0, 22.0)
727                        .mid_bottom_with_margin_on(state.ids.btn_link, -27.0)
728                        .down_from(state.ids.btn_link, 5.0)
729                        .hover_image(self.imgs.button_hover)
730                        .press_image(self.imgs.button_press)
731                        .label(&self.localized_strings.get_msg("hud-group-kick"))
732                        .label_color(if state.selected_member.is_some() {
733                            TEXT_COLOR
734                        } else {
735                            TEXT_COLOR_GREY
736                        })
737                        .label_font_id(self.fonts.cyri.conrod_id)
738                        .label_font_size(self.fonts.cyri.scale(10))
739                        .set(state.ids.btn_kick, ui)
740                        .was_clicked()
741                    {
742                        if let Some(uid) = selected {
743                            events.push(Event::Kick(uid));
744                            state.update(|s| {
745                                s.selected_member = None;
746                            });
747                        }
748                    };
749                }
750                // Group Members, only character names, cut long names when they exceed the
751                // button size
752                let group_size = group_members.len();
753                if state.ids.members.len() < group_size {
754                    state.update(|s| {
755                        s.ids
756                            .members
757                            .resize(group_size, &mut ui.widget_id_generator())
758                    })
759                }
760                // Scrollable area for group member names
761                Rectangle::fill_with([110.0, 135.0], color::TRANSPARENT)
762                    .top_left_with_margins_on(state.ids.bg, 5.0, 5.0)
763                    .crop_kids()
764                    .scroll_kids_vertically()
765                    .set(state.ids.scroll_area, ui);
766                Scrollbar::y_axis(state.ids.scroll_area)
767                    .thickness(5.0)
768                    .rgba(0.33, 0.33, 0.33, 1.0)
769                    .set(state.ids.scrollbar, ui);
770                // List member names
771                for (i, &uid) in group_members.iter().copied().enumerate() {
772                    let selected = state.selected_member == Some(uid);
773                    let char_name = uid_to_name_text(uid, self.client);
774                    // TODO: Do something special visually if uid == leader
775                    if Button::image(if selected {
776                        self.imgs.selection
777                    } else {
778                        self.imgs.nothing
779                    })
780                    .w_h(100.0, 22.0)
781                    .and(|w| {
782                        if i == 0 {
783                            w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0)
784                        } else {
785                            w.down_from(state.ids.members[i - 1], 5.0)
786                        }
787                    })
788                    .hover_image(self.imgs.selection_hover)
789                    .press_image(self.imgs.selection_press)
790                    .image_color(color::rgba(1.0, 0.82, 0.27, 1.0))
791                    .crop_kids()
792                    .label_x(Relative::Place(Place::Start(Some(4.0))))
793                    .label(&char_name)
794                    .label_color(if uid == leader {
795                        ERROR_COLOR
796                    } else {
797                        TEXT_COLOR
798                    })
799                    .label_font_id(self.fonts.cyri.conrod_id)
800                    .label_font_size(self.fonts.cyri.scale(12))
801                    .set(state.ids.members[i], ui)
802                    .was_clicked()
803                    {
804                        // Do nothing when clicking yourself
805                        if Some(uid) != my_uid {
806                            // Select the group member
807                            state.update(|s| {
808                                s.selected_member = if selected { None } else { Some(uid) }
809                            });
810                        }
811                    };
812                }
813                // Maximum of 6 Players/Npcs per Group
814                // Player pets count as group members, too. They are not counted
815                // into the maximum group size.
816            }
817        }
818        if let Some((invite_uid, _, _, kind)) = open_invite {
819            self.show.group = true; // Auto open group menu
820            // TODO: add group name here too
821            // Invite text
822
823            let name = uid_to_name_text(invite_uid, self.client);
824            let invite_text = match kind {
825                InviteKind::Group => self.localized_strings.get_msg_ctx(
826                    "hud-group-invite_to_join",
827                    &i18n::fluent_args! {
828                        "name" => name,
829                    },
830                ),
831                InviteKind::Trade => self.localized_strings.get_msg_ctx(
832                    "hud-group-invite_to_trade",
833                    &i18n::fluent_args! {
834                        "name" => &name,
835                    },
836                ),
837            };
838            Text::new(&invite_text)
839                .mid_top_with_margin_on(state.ids.bg, 5.0)
840                .font_size(12)
841                .font_id(self.fonts.cyri.conrod_id)
842                .color(TEXT_COLOR)
843                .w(165.0) // Text stays within frame
844                .set(state.ids.title, ui);
845            // Accept Button
846            let accept_key = self
847                .settings
848                .controls
849                .get_binding(GameInput::AcceptGroupInvite)
850                .map_or_else(|| "".into(), |key| key.display_string());
851            if Button::image(self.imgs.button)
852                .w_h(90.0, 22.0)
853                .bottom_left_with_margins_on(state.ids.bg, 15.0, 15.0)
854                .hover_image(self.imgs.button_hover)
855                .press_image(self.imgs.button_press)
856                .label(&format!(
857                    "[{}] {}",
858                    &accept_key,
859                    self.localized_strings.get_msg("common-accept")
860                ))
861                .label_color(TEXT_COLOR)
862                .label_font_id(self.fonts.cyri.conrod_id)
863                .label_font_size(self.fonts.cyri.scale(12))
864                .set(state.ids.btn_accept, ui)
865                .was_clicked()
866            {
867                events.push(Event::Accept);
868                self.show.group_menu = true;
869            };
870            // Decline button
871            let decline_key = self
872                .settings
873                .controls
874                .get_binding(GameInput::DeclineGroupInvite)
875                .map_or_else(|| "".into(), |key| key.display_string());
876            if Button::image(self.imgs.button)
877                .w_h(90.0, 22.0)
878                .bottom_right_with_margins_on(state.ids.bg, 15.0, 15.0)
879                .hover_image(self.imgs.button_hover)
880                .press_image(self.imgs.button_press)
881                .label(&format!(
882                    "[{}] {}",
883                    &decline_key,
884                    self.localized_strings.get_msg("common-decline")
885                ))
886                .label_color(TEXT_COLOR)
887                .label_font_id(self.fonts.cyri.conrod_id)
888                .label_font_size(self.fonts.cyri.scale(12))
889                .set(state.ids.btn_decline, ui)
890                .was_clicked()
891            {
892                events.push(Event::Decline);
893            };
894        }
895
896        events
897    }
898}