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 key_layout = &self.global_state.window.key_layout;
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
198                .character
199                .as_ref()
200                .map_or_else(|| format!("Player<{}>", uid), |c| c.name.clone()),
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| stats.name.clone())
212                })
213                .unwrap_or_else(|| format!("Npc<{}>", uid)),
214        };
215
216        let open_invite = self.client.invite().and_then(|invite| {
217            // Don't show invite if it comes from a muted player
218            if self
219                .client
220                .player_list()
221                .get(&invite.0)
222                .is_none_or(|player| {
223                    self.global_state
224                        .profile
225                        .mutelist
226                        .contains_key(&player.uuid)
227                })
228            {
229                None
230            } else {
231                Some(invite)
232            }
233        });
234
235        let my_uid = self.client.uid();
236
237        // TODO show something to the player when they click on the group button while
238        // they are not in a group so that it doesn't look like the button is
239        // broken
240        if self.show.group_menu || open_invite.is_some() {
241            // Frame
242            Rectangle::fill_with([220.0, 140.0], Color::Rgba(0.0, 0.0, 0.0, 0.8))
243                .bottom_left_with_margins_on(ui.window, 108.0, 490.0)
244                .crop_kids()
245                .set(state.ids.bg, ui);
246        }
247        if let Some((_, timeout_start, timeout_dur, _)) = open_invite {
248            // Group Menu button
249            Button::image(self.imgs.group_icon)
250                .w_h(49.0, 26.0)
251                .bottom_left_with_margins_on(ui.window, 10.0, 490.0)
252                .set(state.ids.group_button, ui);
253            // Show timeout bar
254            let timeout_progress =
255                1.0 - timeout_start.elapsed().as_secs_f32() / timeout_dur.as_secs_f32();
256            Image::new(self.imgs.progress_frame)
257                .w_h(100.0, 10.0)
258                .middle_of(state.ids.bg)
259                .color(Some(UI_MAIN))
260                .set(state.ids.timeout_bg, ui);
261            Image::new(self.imgs.progress)
262                .w_h(98.0 * timeout_progress as f64, 8.0)
263                .top_left_with_margins_on(state.ids.timeout_bg, 1.0, 1.0)
264                .color(Some(UI_HIGHLIGHT_0))
265                .set(state.ids.timeout, ui);
266        }
267        // Buttons
268        if let Some((group_name, leader)) = self.client.group_info().filter(|_| in_group) {
269            // Group Menu Button
270            if Button::image(if self.show.group_menu {
271                self.imgs.group_icon_press
272            } else {
273                self.imgs.group_icon
274            })
275            .w_h(49.0, 26.0)
276            .bottom_left_with_margins_on(ui.window, 10.0, 490.0)
277            .hover_image(self.imgs.group_icon_hover)
278            .press_image(self.imgs.group_icon_press)
279            .set(state.ids.group_button, ui)
280            .was_clicked()
281            {
282                self.show.group_menu = !self.show.group_menu;
283            };
284            Text::new(&group_name)
285                .up_from(state.ids.group_button, 5.0)
286                .font_size(14)
287                .font_id(self.fonts.cyri.conrod_id)
288                .color(BLACK)
289                .set(state.ids.title_bg, ui);
290            Text::new(&group_name)
291                .bottom_right_with_margins_on(state.ids.title_bg, 1.0, 1.0)
292                .font_size(14)
293                .font_id(self.fonts.cyri.conrod_id)
294                .color(TEXT_COLOR)
295                .set(state.ids.title, ui);
296            // Member panels
297            let group_size = group_members.len();
298            if state.ids.member_panels_bg.len() < group_size {
299                state.update(|s| {
300                    s.ids
301                        .member_panels_bg
302                        .resize(group_size, &mut ui.widget_id_generator())
303                })
304            };
305            if state.ids.member_health.len() < group_size {
306                state.update(|s| {
307                    s.ids
308                        .member_health
309                        .resize(group_size, &mut ui.widget_id_generator());
310                })
311            };
312            if state.ids.member_health_decay.len() < group_size {
313                state.update(|s| {
314                    s.ids
315                        .member_health_decay
316                        .resize(group_size, &mut ui.widget_id_generator());
317                })
318            };
319            if state.ids.member_energy.len() < group_size {
320                state.update(|s| {
321                    s.ids
322                        .member_energy
323                        .resize(group_size, &mut ui.widget_id_generator())
324                })
325            };
326            if state.ids.member_panels_frame.len() < group_size {
327                state.update(|s| {
328                    s.ids
329                        .member_panels_frame
330                        .resize(group_size, &mut ui.widget_id_generator())
331                })
332            };
333            if state.ids.member_panels_txt.len() < group_size {
334                state.update(|s| {
335                    s.ids
336                        .member_panels_txt
337                        .resize(group_size, &mut ui.widget_id_generator())
338                })
339            };
340            if state.ids.dead_txt.len() < group_size {
341                state.update(|s| {
342                    s.ids
343                        .dead_txt
344                        .resize(group_size, &mut ui.widget_id_generator())
345                })
346            };
347            if state.ids.health_txt.len() < group_size {
348                state.update(|s| {
349                    s.ids
350                        .health_txt
351                        .resize(group_size, &mut ui.widget_id_generator())
352                })
353            };
354            if state.ids.member_panels_txt_bg.len() < group_size {
355                state.update(|s| {
356                    s.ids
357                        .member_panels_txt_bg
358                        .resize(group_size, &mut ui.widget_id_generator())
359                })
360            };
361            if state.ids.combat_rating_indicators.len() < group_size {
362                state.update(|s| {
363                    s.ids
364                        .combat_rating_indicators
365                        .resize(group_size, &mut ui.widget_id_generator())
366                })
367            };
368            if state.ids.hardcore_indicators.len() < group_size {
369                state.update(|s| {
370                    s.ids
371                        .hardcore_indicators
372                        .resize(group_size, &mut ui.widget_id_generator())
373                })
374            };
375            let client_state = self.client.state();
376            let stats = client_state.ecs().read_storage::<Stats>();
377            let skill_sets = client_state.ecs().read_storage::<common::comp::SkillSet>();
378            let healths = client_state.ecs().read_storage::<common::comp::Health>();
379            let energy = client_state.ecs().read_storage::<common::comp::Energy>();
380            let buffs = client_state.ecs().read_storage::<common::comp::Buffs>();
381            let inventory = client_state.ecs().read_storage::<common::comp::Inventory>();
382            let id_maps = client_state.ecs().read_resource::<IdMaps>();
383            let bodies = client_state.ecs().read_storage::<common::comp::Body>();
384            let poises = client_state.ecs().read_storage::<common::comp::Poise>();
385            let stances = client_state.ecs().read_storage::<common::comp::Stance>();
386            let hardcore = client_state.ecs().read_storage::<common::comp::Hardcore>();
387
388            // Keep track of the total number of widget ids we are using for buffs
389            let mut total_buff_count = 0;
390            for (i, &uid) in group_members.iter().copied().enumerate() {
391                self.show.group = true;
392                let entity = id_maps.uid_entity(uid);
393                let stats = entity.and_then(|entity| stats.get(entity));
394                let skill_set = entity.and_then(|entity| skill_sets.get(entity));
395                let health = entity.and_then(|entity| healths.get(entity));
396                let energy = entity.and_then(|entity| energy.get(entity));
397                let buffs = entity.and_then(|entity| buffs.get(entity));
398                let inventory = entity.and_then(|entity| inventory.get(entity));
399                let is_leader = uid == leader;
400                let body = entity.and_then(|entity| bodies.get(entity));
401                let poise = entity.and_then(|entity| poises.get(entity));
402                let stance = entity.and_then(|entity| stances.get(entity));
403                let hardcore = entity.and_then(|entity| hardcore.get(entity));
404
405                if let (
406                    Some(stats),
407                    Some(skill_set),
408                    Some(inventory),
409                    Some(health),
410                    Some(energy),
411                    Some(body),
412                    Some(poise),
413                ) = (stats, skill_set, inventory, health, energy, body, poise)
414                {
415                    let combat_rating = combat::combat_rating(
416                        inventory, health, energy, poise, skill_set, *body, self.msm,
417                    );
418                    let char_name = stats.name.to_string();
419                    let health_perc = health.current() / health.base_max().max(health.maximum());
420                    // change panel positions when debug info is shown
421                    let x = if debug_on { i / 8 } else { i / 11 };
422                    let y = if debug_on { i % 8 } else { i % 11 };
423                    let back = Image::new(self.imgs.member_bg).top_left_with_margins_on(
424                        ui.window,
425                        50.0 + offset + y as f64 * 77.0,
426                        10.0 + x as f64 * 180.0,
427                    );
428                    let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
429                    let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
430                    let health_col = match (health_perc * 100.0) as u8 {
431                        0..=20 => crit_hp_color,
432                        21..=40 => LOW_HP_COLOR,
433                        _ => HP_COLOR,
434                    };
435                    // Don't show panel for the player!
436                    // Panel BG
437                    back.w_h(152.0, 36.0)
438                        .color(if is_leader {
439                            Some(ERROR_COLOR)
440                        } else {
441                            Some(TEXT_COLOR)
442                        })
443                        .set(state.ids.member_panels_bg[i], ui);
444                    // Health
445                    Image::new(self.imgs.bar_content)
446                        .w_h(148.0 * f64::from(health_perc), 22.0)
447                        .color(Some(health_col))
448                        .top_left_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0)
449                        .set(state.ids.member_health[i], ui);
450                    // Health Decay
451                    let decayed_health = f64::from(1.0 - health.maximum() / health.base_max());
452                    if decayed_health > 0.0 {
453                        let decay_bar_len = 148.0 * decayed_health;
454                        Image::new(self.imgs.bar_content)
455                            .w_h(decay_bar_len, 22.0)
456                            .color(Some(QUALITY_EPIC))
457                            .top_right_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0)
458                            .set(state.ids.member_health_decay[i], ui);
459                    }
460                    if health.is_dead {
461                        // Death Text
462                        Text::new(&self.localized_strings.get_msg("hud-group-dead"))
463                            .mid_top_with_margin_on(state.ids.member_panels_bg[i], 1.0)
464                            .font_size(20)
465                            .font_id(self.fonts.cyri.conrod_id)
466                            .color(KILL_COLOR)
467                            .set(state.ids.dead_txt[i], ui);
468                    } else {
469                        // Health Text
470                        let txt = format!(
471                            "{}/{}",
472                            health.current().round() as u32,
473                            health.maximum().round() as u32,
474                        );
475                        // Change font size depending on health amount
476                        let font_size = match health.maximum() {
477                            x if (0.0..100.0).contains(&x) => 14,
478                            x if (100.0..=1000.0).contains(&x) => 13,
479                            x if (1000.0..=10000.0).contains(&x) => 12,
480                            _ => 11,
481                        };
482                        // Change text offset depending on health amount
483                        let txt_offset = match health.maximum() {
484                            x if (0.0..=100.0).contains(&x) => 4.0,
485                            x if (100.0..=1000.0).contains(&x) => 4.5,
486                            x if (1000.0..=10000.0).contains(&x) => 5.0,
487                            _ => 5.5,
488                        };
489                        Text::new(&txt)
490                            .mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset)
491                            .font_size(font_size)
492                            .font_id(self.fonts.cyri.conrod_id)
493                            .color(Color::Rgba(1.0, 1.0, 1.0, 0.5))
494                            .set(state.ids.health_txt[i], ui);
495                    };
496
497                    // Panel Frame
498                    Image::new(self.imgs.member_frame)
499                        .w_h(152.0, 36.0)
500                        .middle_of(state.ids.member_panels_bg[i])
501                        .color(Some(UI_HIGHLIGHT_0))
502                        .set(state.ids.member_panels_frame[i], ui);
503
504                    let indicator_col = cr_color(combat_rating);
505                    Image::new(self.imgs.combat_rating_ico_shadow)
506                        .w_h(18.0, 18.0)
507                        .top_left_with_margins_on(state.ids.member_panels_frame[i], -20.0, 2.0)
508                        .color(Some(indicator_col))
509                        .set(state.ids.combat_rating_indicators[i], ui);
510                    if hardcore.is_some() {
511                        Image::new(self.imgs.hardcore)
512                            .w_h(18.0, 18.0)
513                            .top_left_with_margins_on(state.ids.member_panels_frame[i], -20.0, 22.0)
514                            .set(state.ids.hardcore_indicators[i], ui);
515                    }
516                    // Panel Text
517                    Text::new(&char_name)
518                     .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 22.0 + hardcore.map_or(0.0, |_| 20.0))
519                     .font_size(20)
520                     .font_id(self.fonts.cyri.conrod_id)
521                     .color(BLACK)
522                     .w(300.0) // limit name length display
523                     .set(state.ids.member_panels_txt_bg[i], ui);
524                    Text::new(&char_name)
525                            .bottom_left_with_margins_on(state.ids.member_panels_txt_bg[i], 2.0, 2.0)
526                            .font_size(20)
527                            .font_id(self.fonts.cyri.conrod_id)
528                            .color(if is_leader { ERROR_COLOR } else { GROUP_COLOR })
529                            .w(300.0) // limit name length display
530                            .set(state.ids.member_panels_txt[i], ui);
531                    let stam_perc = energy.current() / energy.maximum();
532                    // Energy
533                    Image::new(self.imgs.bar_content)
534                        .w_h(100.0 * f64::from(stam_perc), 8.0)
535                        .color(Some(STAMINA_COLOR))
536                        .top_left_with_margins_on(state.ids.member_panels_bg[i], 26.0, 2.0)
537                        .set(state.ids.member_energy[i], ui);
538                    if let Some(buffs) = buffs {
539                        let buff_icons = BuffIcon::icons_vec(buffs, stance);
540                        // Limit displayed buffs to 11
541                        let buff_count = buff_icons.len().min(11);
542                        total_buff_count += buff_count;
543                        let gen = &mut ui.widget_id_generator();
544                        if state.ids.buffs.len() < total_buff_count {
545                            state.update(|state| state.ids.buffs.resize(total_buff_count, gen));
546                        }
547                        if state.ids.buff_timers.len() < total_buff_count {
548                            state.update(|state| {
549                                state.ids.buff_timers.resize(total_buff_count, gen)
550                            });
551                        }
552                        // Create Buff Widgets
553                        let mut prev_id = None;
554                        state
555                            .ids
556                            .buffs
557                            .iter()
558                            .copied()
559                            .zip(state.ids.buff_timers.iter().copied())
560                            .skip(total_buff_count - buff_count)
561                            .zip(buff_icons.iter())
562                            .for_each(|((id, timer_id), buff)| {
563                                let max_duration = buff.kind.max_duration();
564                                let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
565                                let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
566                                let current_duration = buff.end_time.map(|end| end - self.time.0);
567                                let duration_percentage = current_duration.map_or(1000.0, |cur| {
568                                    max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
569                                }) as u32; // Percentage to determine which frame of the timer overlay is displayed
570                                let buff_img = buff.kind.image(self.imgs);
571                                let buff_widget = Image::new(buff_img).w_h(15.0, 15.0);
572                                let buff_widget = if let Some(id) = prev_id {
573                                    buff_widget.right_from(id, 1.0)
574                                } else {
575                                    buff_widget.bottom_left_with_margins_on(
576                                        state.ids.member_panels_frame[i],
577                                        -16.0,
578                                        1.0,
579                                    )
580                                };
581                                prev_id = Some(id);
582                                buff_widget
583                                    .color(if current_duration.is_some_and(|cur| cur < 10.0) {
584                                        Some(pulsating_col)
585                                    } else {
586                                        Some(norm_col)
587                                    })
588                                    .set(id, ui);
589                                // Create Buff tooltip
590                                let (title, desc_txt) =
591                                    buff.kind.title_description(localized_strings);
592                                let remaining_time = buff.get_buff_time(*self.time);
593                                let desc = format!("{}\n\n{}", desc_txt, remaining_time);
594                                Image::new(match duration_percentage as u64 {
595                                    875..=1000 => self.imgs.nothing, // 8/8
596                                    750..=874 => self.imgs.buff_0,   // 7/8
597                                    625..=749 => self.imgs.buff_1,   // 6/8
598                                    500..=624 => self.imgs.buff_2,   // 5/8
599                                    375..=499 => self.imgs.buff_3,   // 4/8
600                                    250..=374 => self.imgs.buff_4,   // 3/8
601                                    125..=249 => self.imgs.buff_5,   // 2/8
602                                    0..=124 => self.imgs.buff_6,     // 1/8
603                                    _ => self.imgs.nothing,
604                                })
605                                .w_h(15.0, 15.0)
606                                .middle_of(id)
607                                .with_tooltip(
608                                    self.tooltip_manager,
609                                    &title,
610                                    &desc,
611                                    &buffs_tooltip,
612                                    if buff.is_buff {
613                                        BUFF_COLOR
614                                    } else {
615                                        DEBUFF_COLOR
616                                    },
617                                )
618                                .set(timer_id, ui);
619                            });
620                    } else {
621                        // Values N.A.
622                        Text::new(&stats.name.to_string())
623                            .top_left_with_margins_on(state.ids.member_panels_frame[i], -22.0, 0.0)
624                            .font_size(20)
625                            .font_id(self.fonts.cyri.conrod_id)
626                            .color(GROUP_COLOR)
627                            .set(state.ids.member_panels_txt[i], ui);
628                        let back = if i == 0 {
629                            Image::new(self.imgs.member_bg)
630                                .top_left_with_margins_on(ui.window, offset, 20.0)
631                        } else {
632                            Image::new(self.imgs.member_bg)
633                                .down_from(state.ids.member_panels_bg[i - 1], 40.0)
634                        };
635                        back.w_h(152.0, 36.0)
636                            .color(Some(TEXT_COLOR))
637                            .set(state.ids.member_panels_bg[i], ui);
638                        // Panel Frame
639                        Image::new(self.imgs.member_frame)
640                            .w_h(152.0, 36.0)
641                            .middle_of(state.ids.member_panels_bg[i])
642                            .color(Some(UI_HIGHLIGHT_0))
643                            .set(state.ids.member_panels_frame[i], ui);
644                        // Panel Text
645                        Text::new(&self.localized_strings.get_msg("hud-group-out_of_range"))
646                            .mid_top_with_margin_on(state.ids.member_panels_bg[i], 3.0)
647                            .font_size(16)
648                            .font_id(self.fonts.cyri.conrod_id)
649                            .color(TEXT_COLOR)
650                            .set(state.ids.dead_txt[i], ui);
651                    }
652                }
653            }
654
655            if self.show.group_menu {
656                let selected = state.selected_member;
657                if Button::image(self.imgs.button) // Change button behaviour and style when the friendslist is working
658                    .w_h(90.0, 22.0)
659                    .top_right_with_margins_on(state.ids.bg, 5.0, 5.0)
660                    .hover_image(self.imgs.button)
661                    .press_image(self.imgs.button)
662                    .label_color(TEXT_COLOR_GREY)
663                    .image_color(TEXT_COLOR_GREY)
664                    .label(&self.localized_strings.get_msg("hud-group-add_friend"))
665                    .label_font_id(self.fonts.cyri.conrod_id)
666                    .label_font_size(self.fonts.cyri.scale(10))
667                    .set(state.ids.btn_friend, ui)
668                    .was_clicked()
669                {};
670                if Button::image(self.imgs.button)
671                    .w_h(90.0, 22.0)
672                    .bottom_right_with_margins_on(state.ids.bg, 5.0, 5.0)
673                    .hover_image(self.imgs.button_hover)
674                    .press_image(self.imgs.button_press)
675                    .label(&self.localized_strings.get_msg("hud-group-leave"))
676                    .label_color(TEXT_COLOR)
677                    .label_font_id(self.fonts.cyri.conrod_id)
678                    .label_font_size(self.fonts.cyri.scale(10))
679                    .set(state.ids.btn_leave, ui)
680                    .was_clicked()
681                {
682                    self.show.group_menu = false;
683                    self.show.group = !self.show.group;
684                    events.push(Event::LeaveGroup);
685                };
686                // Group leader functions
687                if my_uid == Some(leader) {
688                    if Button::image(self.imgs.button)
689                        .w_h(90.0, 22.0)
690                        .mid_bottom_with_margin_on(state.ids.btn_friend, -27.0)
691                        .hover_image(self.imgs.button_hover)
692                        .press_image(self.imgs.button_press)
693                        .label(&self.localized_strings.get_msg("hud-group-assign_leader"))
694                        .label_color(if state.selected_member.is_some() {
695                            TEXT_COLOR
696                        } else {
697                            TEXT_COLOR_GREY
698                        })
699                        .label_font_id(self.fonts.cyri.conrod_id)
700                        .label_font_size(self.fonts.cyri.scale(10))
701                        .set(state.ids.btn_leader, ui)
702                        .was_clicked()
703                    {
704                        if let Some(uid) = selected {
705                            events.push(Event::AssignLeader(uid));
706                            state.update(|s| {
707                                s.selected_member = None;
708                            });
709                        }
710                    };
711                    if Button::image(self.imgs.button)
712                        .w_h(90.0, 22.0)
713                        .mid_bottom_with_margin_on(state.ids.btn_leader, -27.0)
714                        .hover_image(self.imgs.button)
715                        .press_image(self.imgs.button)
716                        .label(&self.localized_strings.get_msg("hud-group-link_group"))
717                        .hover_image(self.imgs.button)
718                        .press_image(self.imgs.button)
719                        .label_color(TEXT_COLOR_GREY)
720                        .image_color(TEXT_COLOR_GREY)
721                        .label_font_id(self.fonts.cyri.conrod_id)
722                        .label_font_size(self.fonts.cyri.scale(10))
723                        .set(state.ids.btn_link, ui)
724                        .was_clicked()
725                    {};
726                    if Button::image(self.imgs.button)
727                        .w_h(90.0, 22.0)
728                        .mid_bottom_with_margin_on(state.ids.btn_link, -27.0)
729                        .down_from(state.ids.btn_link, 5.0)
730                        .hover_image(self.imgs.button_hover)
731                        .press_image(self.imgs.button_press)
732                        .label(&self.localized_strings.get_msg("hud-group-kick"))
733                        .label_color(if state.selected_member.is_some() {
734                            TEXT_COLOR
735                        } else {
736                            TEXT_COLOR_GREY
737                        })
738                        .label_font_id(self.fonts.cyri.conrod_id)
739                        .label_font_size(self.fonts.cyri.scale(10))
740                        .set(state.ids.btn_kick, ui)
741                        .was_clicked()
742                    {
743                        if let Some(uid) = selected {
744                            events.push(Event::Kick(uid));
745                            state.update(|s| {
746                                s.selected_member = None;
747                            });
748                        }
749                    };
750                }
751                // Group Members, only character names, cut long names when they exceed the
752                // button size
753                let group_size = group_members.len();
754                if state.ids.members.len() < group_size {
755                    state.update(|s| {
756                        s.ids
757                            .members
758                            .resize(group_size, &mut ui.widget_id_generator())
759                    })
760                }
761                // Scrollable area for group member names
762                Rectangle::fill_with([110.0, 135.0], color::TRANSPARENT)
763                    .top_left_with_margins_on(state.ids.bg, 5.0, 5.0)
764                    .crop_kids()
765                    .scroll_kids_vertically()
766                    .set(state.ids.scroll_area, ui);
767                Scrollbar::y_axis(state.ids.scroll_area)
768                    .thickness(5.0)
769                    .rgba(0.33, 0.33, 0.33, 1.0)
770                    .set(state.ids.scrollbar, ui);
771                // List member names
772                for (i, &uid) in group_members.iter().copied().enumerate() {
773                    let selected = state.selected_member == Some(uid);
774                    let char_name = uid_to_name_text(uid, self.client);
775                    // TODO: Do something special visually if uid == leader
776                    if Button::image(if selected {
777                        self.imgs.selection
778                    } else {
779                        self.imgs.nothing
780                    })
781                    .w_h(100.0, 22.0)
782                    .and(|w| {
783                        if i == 0 {
784                            w.top_left_with_margins_on(state.ids.scroll_area, 5.0, 0.0)
785                        } else {
786                            w.down_from(state.ids.members[i - 1], 5.0)
787                        }
788                    })
789                    .hover_image(self.imgs.selection_hover)
790                    .press_image(self.imgs.selection_press)
791                    .image_color(color::rgba(1.0, 0.82, 0.27, 1.0))
792                    .crop_kids()
793                    .label_x(Relative::Place(Place::Start(Some(4.0))))
794                    .label(&char_name)
795                    .label_color(if uid == leader {
796                        ERROR_COLOR
797                    } else {
798                        TEXT_COLOR
799                    })
800                    .label_font_id(self.fonts.cyri.conrod_id)
801                    .label_font_size(self.fonts.cyri.scale(12))
802                    .set(state.ids.members[i], ui)
803                    .was_clicked()
804                    {
805                        // Do nothing when clicking yourself
806                        if Some(uid) != my_uid {
807                            // Select the group member
808                            state.update(|s| {
809                                s.selected_member = if selected { None } else { Some(uid) }
810                            });
811                        }
812                    };
813                }
814                // Maximum of 6 Players/Npcs per Group
815                // Player pets count as group members, too. They are not counted
816                // into the maximum group size.
817            }
818        }
819        if let Some((invite_uid, _, _, kind)) = open_invite {
820            self.show.group = true; // Auto open group menu
821            // TODO: add group name here too
822            // Invite text
823
824            let name = uid_to_name_text(invite_uid, self.client);
825            let invite_text = match kind {
826                InviteKind::Group => self.localized_strings.get_msg_ctx(
827                    "hud-group-invite_to_join",
828                    &i18n::fluent_args! {
829                        "name" => name,
830                    },
831                ),
832                InviteKind::Trade => self.localized_strings.get_msg_ctx(
833                    "hud-group-invite_to_trade",
834                    &i18n::fluent_args! {
835                        "name" => &name,
836                    },
837                ),
838            };
839            Text::new(&invite_text)
840                .mid_top_with_margin_on(state.ids.bg, 5.0)
841                .font_size(12)
842                .font_id(self.fonts.cyri.conrod_id)
843                .color(TEXT_COLOR)
844                .w(165.0) // Text stays within frame
845                .set(state.ids.title, ui);
846            // Accept Button
847            let accept_key = self
848                .settings
849                .controls
850                .get_binding(GameInput::AcceptGroupInvite)
851                .map_or_else(|| "".into(), |key| key.display_string(key_layout));
852            if Button::image(self.imgs.button)
853                .w_h(90.0, 22.0)
854                .bottom_left_with_margins_on(state.ids.bg, 15.0, 15.0)
855                .hover_image(self.imgs.button_hover)
856                .press_image(self.imgs.button_press)
857                .label(&format!(
858                    "[{}] {}",
859                    &accept_key,
860                    self.localized_strings.get_msg("common-accept")
861                ))
862                .label_color(TEXT_COLOR)
863                .label_font_id(self.fonts.cyri.conrod_id)
864                .label_font_size(self.fonts.cyri.scale(12))
865                .set(state.ids.btn_accept, ui)
866                .was_clicked()
867            {
868                events.push(Event::Accept);
869                self.show.group_menu = true;
870            };
871            // Decline button
872            let decline_key = self
873                .settings
874                .controls
875                .get_binding(GameInput::DeclineGroupInvite)
876                .map_or_else(|| "".into(), |key| key.display_string(key_layout));
877            if Button::image(self.imgs.button)
878                .w_h(90.0, 22.0)
879                .bottom_right_with_margins_on(state.ids.bg, 15.0, 15.0)
880                .hover_image(self.imgs.button_hover)
881                .press_image(self.imgs.button_press)
882                .label(&format!(
883                    "[{}] {}",
884                    &decline_key,
885                    self.localized_strings.get_msg("common-decline")
886                ))
887                .label_color(TEXT_COLOR)
888                .label_font_id(self.fonts.cyri.conrod_id)
889                .label_font_size(self.fonts.cyri.scale(12))
890                .set(state.ids.btn_decline, ui)
891                .was_clicked()
892            {
893                events.push(Event::Decline);
894            };
895        }
896
897        events
898    }
899}