Skip to main content

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