Skip to main content

veloren_voxygen/hud/
skillbar.rs

1use super::{
2    BLACK, BarNumbers, CRITICAL_HP_COLOR, HP_COLOR, HudInfo, LOW_HP_COLOR, POISE_COLOR,
3    POISEBAR_TICK_COLOR, QUALITY_EPIC, QUALITY_LEGENDARY, STAMINA_COLOR, ShortcutNumbers,
4    TEXT_COLOR, TEXT_VELORITE, UI_HIGHLIGHT_0, XP_COLOR, hotbar,
5    img_ids::{Imgs, ImgsRot},
6    item_imgs::ItemImgs,
7    slots, util,
8};
9use crate::{
10    GlobalState,
11    game_input::GameInput,
12    hud::{
13        ComboFloater, Position, PositionSpecifier, animation::animation_timer,
14        controller_icons as icon_utils,
15    },
16    key_state::GIVE_UP_HOLD_TIME,
17    ui::{
18        ImageFrame, ItemTooltip, ItemTooltipManager, ItemTooltipable, Tooltip, TooltipManager,
19        Tooltipable,
20        fonts::Fonts,
21        slot::{ContentSize, SlotMaker},
22    },
23    window::{KeyMouse, LastInput},
24};
25use i18n::Localization;
26
27use client::{self, Client};
28use common::{
29    comp::{
30        self, Ability, ActiveAbilities, Body, Buffs, CharacterState, Combo, Energy, Hardcore,
31        Health, Inventory, Poise, PoiseState, SkillSet, Stats,
32        ability::{AbilityInput, Stance},
33        is_downed,
34        item::{ItemDesc, ItemI18n, MaterialStatManifest, tool::ToolKind},
35        skillset::SkillGroupKind,
36    },
37    recipe::RecipeBookManifest,
38};
39use conrod_core::{
40    Color, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
41    widget::{self, Button, Image, Rectangle, Text},
42    widget_ids,
43};
44use vek::*;
45
46widget_ids! {
47    struct Ids {
48        // Death message
49        death_message_1,
50        death_message_2,
51        death_message_1_bg,
52        death_message_2_bg,
53        death_message_3,
54        death_message_3_bg,
55        // Skillbar
56        frame,
57        bg_health,
58        frame_health,
59        bg_energy,
60        frame_energy,
61        bg_poise,
62        frame_poise,
63        m1_ico,
64        m2_ico,
65        // HP-Bar
66        hp_alignment,
67        hp_filling,
68        hp_decayed,
69        hp_txt_bg,
70        hp_txt,
71        decay_overlay,
72        // Energy-Bar
73        energy_alignment,
74        energy_filling,
75        energy_txt_bg,
76        energy_txt,
77        // Poise-Bar
78        poise_alignment,
79        poise_filling,
80        poise_ticks[],
81        poise_txt_bg,
82        poise_txt,
83        // Exp-Bar
84        exp_frame_bg,
85        exp_frame,
86        exp_filling,
87        exp_img_frame_bg,
88        exp_img_frame,
89        exp_img,
90        exp_lvl,
91        diary_txt_bg,
92        diary_txt,
93        sp_arrow,
94        sp_arrow_txt_bg,
95        sp_arrow_txt,
96        // Bag Button
97        bag_frame,
98        bag_filling,
99        bag_img_frame_bg,
100        bag_img_frame,
101        bag_img,
102        bag_space_bg,
103        bag_space,
104        bag_numbers_alignment,
105        bag_text_bg,
106        bag_text,
107        // Combo Counter
108        combo_align,
109        combo_bg,
110        combo,
111        // Slots
112        m1_slot_bg,
113        m1_content,
114        m2_slot_bg,
115        m2_content,
116        slot1,
117        slot1_text,
118        slot1_text_bg,
119        slot2,
120        slot2_text,
121        slot2_text_bg,
122        slot3,
123        slot3_text,
124        slot3_text_bg,
125        slot4,
126        slot4_text,
127        slot4_text_bg,
128        slot5,
129        slot5_text,
130        slot5_text_bg,
131        slot6,
132        slot6_text,
133        slot6_text_bg,
134        slot7,
135        slot7_text,
136        slot7_text_bg,
137        slot8,
138        slot8_text,
139        slot8_text_bg,
140        slot9,
141        slot9_text,
142        slot9_text_bg,
143        slot10,
144        slot10_text,
145        slot10_text_bg,
146        slot_highlight,
147    }
148}
149
150#[derive(Clone, Copy)]
151struct SlotEntry {
152    slot: hotbar::Slot,
153    widget_id: widget::Id,
154    position: PositionSpecifier,
155    game_input: GameInput,
156    shortcut_position: PositionSpecifier,
157    shortcut_position_bg: PositionSpecifier,
158    shortcut_widget_ids: (widget::Id, widget::Id),
159}
160
161fn slot_entries(state: &State, slot_offset: f64) -> [SlotEntry; 10] {
162    use PositionSpecifier::*;
163
164    [
165        // 1th - 5th slots
166        SlotEntry {
167            slot: hotbar::Slot::One,
168            widget_id: state.ids.slot1,
169            position: BottomLeftWithMarginsOn(state.ids.frame, 0.0, 0.0),
170            game_input: GameInput::Slot1,
171            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot1_text_bg, 1.0, 1.0),
172            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot1, 3.0, 5.0),
173            shortcut_widget_ids: (state.ids.slot1_text, state.ids.slot1_text_bg),
174        },
175        SlotEntry {
176            slot: hotbar::Slot::Two,
177            widget_id: state.ids.slot2,
178            position: RightFrom(state.ids.slot1, slot_offset),
179            game_input: GameInput::Slot2,
180            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot2_text_bg, 1.0, 1.0),
181            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot2, 3.0, 5.0),
182            shortcut_widget_ids: (state.ids.slot2_text, state.ids.slot2_text_bg),
183        },
184        SlotEntry {
185            slot: hotbar::Slot::Three,
186            widget_id: state.ids.slot3,
187            position: RightFrom(state.ids.slot2, slot_offset),
188            game_input: GameInput::Slot3,
189            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot3_text_bg, 1.0, 1.0),
190            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot3, 3.0, 5.0),
191            shortcut_widget_ids: (state.ids.slot3_text, state.ids.slot3_text_bg),
192        },
193        SlotEntry {
194            slot: hotbar::Slot::Four,
195            widget_id: state.ids.slot4,
196            position: RightFrom(state.ids.slot3, slot_offset),
197            game_input: GameInput::Slot4,
198            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot4_text_bg, 1.0, 1.0),
199            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot4, 3.0, 5.0),
200            shortcut_widget_ids: (state.ids.slot4_text, state.ids.slot4_text_bg),
201        },
202        SlotEntry {
203            slot: hotbar::Slot::Five,
204            widget_id: state.ids.slot5,
205            position: RightFrom(state.ids.slot4, slot_offset),
206            game_input: GameInput::Slot5,
207            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot5_text_bg, 1.0, 1.0),
208            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot5, 3.0, 5.0),
209            shortcut_widget_ids: (state.ids.slot5_text, state.ids.slot5_text_bg),
210        },
211        // 6th - 10th slots
212        SlotEntry {
213            slot: hotbar::Slot::Six,
214            widget_id: state.ids.slot6,
215            position: RightFrom(state.ids.m2_slot_bg, slot_offset),
216            game_input: GameInput::Slot6,
217            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot6_text_bg, 1.0, 1.0),
218            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot6, 3.0, 5.0),
219            shortcut_widget_ids: (state.ids.slot6_text, state.ids.slot6_text_bg),
220        },
221        SlotEntry {
222            slot: hotbar::Slot::Seven,
223            widget_id: state.ids.slot7,
224            position: RightFrom(state.ids.slot6, slot_offset),
225            game_input: GameInput::Slot7,
226            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot7_text_bg, 1.0, 1.0),
227            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot7, 3.0, 5.0),
228            shortcut_widget_ids: (state.ids.slot7_text, state.ids.slot7_text_bg),
229        },
230        SlotEntry {
231            slot: hotbar::Slot::Eight,
232            widget_id: state.ids.slot8,
233            position: RightFrom(state.ids.slot7, slot_offset),
234            game_input: GameInput::Slot8,
235            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot8_text_bg, 1.0, 1.0),
236            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot8, 3.0, 5.0),
237            shortcut_widget_ids: (state.ids.slot8_text, state.ids.slot8_text_bg),
238        },
239        SlotEntry {
240            slot: hotbar::Slot::Nine,
241            widget_id: state.ids.slot9,
242            position: RightFrom(state.ids.slot8, slot_offset),
243            game_input: GameInput::Slot9,
244            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot9_text_bg, 1.0, 1.0),
245            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot9, 3.0, 5.0),
246            shortcut_widget_ids: (state.ids.slot9_text, state.ids.slot9_text_bg),
247        },
248        SlotEntry {
249            slot: hotbar::Slot::Ten,
250            widget_id: state.ids.slot10,
251            position: RightFrom(state.ids.slot9, slot_offset),
252            game_input: GameInput::Slot10,
253            shortcut_position: BottomLeftWithMarginsOn(state.ids.slot10_text_bg, 1.0, 1.0),
254            shortcut_position_bg: TopRightWithMarginsOn(state.ids.slot10, 3.0, 5.0),
255            shortcut_widget_ids: (state.ids.slot10_text, state.ids.slot10_text_bg),
256        },
257    ]
258}
259
260pub enum Event {
261    OpenDiary(SkillGroupKind),
262    OpenBag,
263}
264
265#[derive(WidgetCommon)]
266pub struct Skillbar<'a> {
267    client: &'a Client,
268    info: &'a HudInfo<'a>,
269    global_state: &'a GlobalState,
270    imgs: &'a Imgs,
271    item_imgs: &'a ItemImgs,
272    fonts: &'a Fonts,
273    rot_imgs: &'a ImgsRot,
274    health: &'a Health,
275    inventory: &'a Inventory,
276    energy: &'a Energy,
277    poise: &'a Poise,
278    skillset: &'a SkillSet,
279    active_abilities: Option<&'a ActiveAbilities>,
280    body: &'a Body,
281    hotbar: &'a hotbar::State,
282    tooltip_manager: &'a mut TooltipManager,
283    item_tooltip_manager: &'a mut ItemTooltipManager,
284    slot_manager: &'a mut slots::SlotManager,
285    localized_strings: &'a Localization,
286    item_i18n: &'a ItemI18n,
287    pulse: f32,
288    #[conrod(common_builder)]
289    common: widget::CommonBuilder,
290    msm: &'a MaterialStatManifest,
291    rbm: &'a RecipeBookManifest,
292    combo_floater: Option<ComboFloater>,
293    combo: Option<&'a Combo>,
294    char_state: Option<&'a CharacterState>,
295    stance: Option<&'a Stance>,
296    stats: Option<&'a Stats>,
297    buffs: Option<&'a Buffs>,
298}
299
300impl<'a> Skillbar<'a> {
301    #[expect(clippy::too_many_arguments)]
302    pub fn new(
303        client: &'a Client,
304        info: &'a HudInfo,
305        global_state: &'a GlobalState,
306        imgs: &'a Imgs,
307        item_imgs: &'a ItemImgs,
308        fonts: &'a Fonts,
309        rot_imgs: &'a ImgsRot,
310        health: &'a Health,
311        inventory: &'a Inventory,
312        energy: &'a Energy,
313        poise: &'a Poise,
314        skillset: &'a SkillSet,
315        active_abilities: Option<&'a ActiveAbilities>,
316        body: &'a Body,
317        pulse: f32,
318        hotbar: &'a hotbar::State,
319        tooltip_manager: &'a mut TooltipManager,
320        item_tooltip_manager: &'a mut ItemTooltipManager,
321        slot_manager: &'a mut slots::SlotManager,
322        localized_strings: &'a Localization,
323        item_i18n: &'a ItemI18n,
324        msm: &'a MaterialStatManifest,
325        rbm: &'a RecipeBookManifest,
326        combo_floater: Option<ComboFloater>,
327        combo: Option<&'a Combo>,
328        char_state: Option<&'a CharacterState>,
329        stance: Option<&'a Stance>,
330        stats: Option<&'a Stats>,
331        buffs: Option<&'a Buffs>,
332    ) -> Self {
333        Self {
334            client,
335            info,
336            global_state,
337            imgs,
338            item_imgs,
339            fonts,
340            rot_imgs,
341            health,
342            inventory,
343            energy,
344            poise,
345            skillset,
346            active_abilities,
347            body,
348            common: widget::CommonBuilder::default(),
349            pulse,
350            hotbar,
351            tooltip_manager,
352            item_tooltip_manager,
353            slot_manager,
354            localized_strings,
355            item_i18n,
356            msm,
357            rbm,
358            combo_floater,
359            combo,
360            char_state,
361            stance,
362            stats,
363            buffs,
364        }
365    }
366
367    fn create_new_button_with_shadow(
368        &self,
369        ui: &mut UiCell,
370        key_mouse: &KeyMouse,
371        button_identifier: widget::Id,
372        text_background: widget::Id,
373        text: widget::Id,
374    ) {
375        let key_desc = key_mouse.display_shortest();
376
377        //Create shadow
378        Text::new(&key_desc)
379            .bottom_right_with_margins_on(button_identifier, 0.0, 0.0)
380            .font_size(10)
381            .font_id(self.fonts.cyri.conrod_id)
382            .color(BLACK)
383            .set(text_background, ui);
384
385        //Create button
386        Text::new(&key_desc)
387            .bottom_right_with_margins_on(text_background, 1.0, 1.0)
388            .font_size(10)
389            .font_id(self.fonts.cyri.conrod_id)
390            .color(TEXT_COLOR)
391            .set(text, ui);
392    }
393
394    fn show_give_up_message(&self, state: &State, ui: &mut UiCell) {
395        let localized_strings = self.localized_strings;
396        let hardcore = self.client.current::<Hardcore>().is_some();
397
398        if let Some(key) = self
399            .global_state
400            .settings
401            .controls
402            .get_binding(GameInput::GiveUp)
403        {
404            let respawn_msg =
405                localized_strings.get_msg_ctx("hud-press_key_to_give_up", &i18n::fluent_args! {
406                    "key" => key.display_string()
407                });
408            let penalty_msg = if hardcore {
409                self.localized_strings
410                    .get_msg("hud-hardcore_will_char_deleted")
411            } else {
412                self.localized_strings.get_msg("hud-items_will_lose_dur")
413            };
414
415            let recieving_help_msg = localized_strings.get_msg("hud-downed_recieving_help");
416            Text::new(&penalty_msg)
417                .mid_bottom_with_margin_on(ui.window, 180.0)
418                .font_size(self.fonts.cyri.scale(30))
419                .font_id(self.fonts.cyri.conrod_id)
420                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
421                .set(state.ids.death_message_3_bg, ui);
422            Text::new(&respawn_msg)
423                .mid_top_with_margin_on(state.ids.death_message_3_bg, -50.0)
424                .font_size(self.fonts.cyri.scale(30))
425                .font_id(self.fonts.cyri.conrod_id)
426                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
427                .set(state.ids.death_message_2_bg, ui);
428            Text::new(&penalty_msg)
429                .bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
430                .font_size(self.fonts.cyri.scale(30))
431                .font_id(self.fonts.cyri.conrod_id)
432                .color(TEXT_COLOR)
433                .set(state.ids.death_message_3, ui);
434            Text::new(&respawn_msg)
435                .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0)
436                .font_size(self.fonts.cyri.scale(30))
437                .font_id(self.fonts.cyri.conrod_id)
438                .color(TEXT_COLOR)
439                .set(state.ids.death_message_2, ui);
440            if self
441                .client
442                .state()
443                .read_storage::<common::interaction::Interactors>()
444                .get(self.client.entity())
445                .is_some_and(|interactors| {
446                    interactors.has_interaction(common::interaction::InteractionKind::HelpDowned)
447                })
448            {
449                Text::new(&recieving_help_msg)
450                    .mid_top_with_margin_on(state.ids.death_message_2_bg, -50.0)
451                    .font_size(self.fonts.cyri.scale(24))
452                    .font_id(self.fonts.cyri.conrod_id)
453                    .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
454                    .set(state.ids.death_message_1_bg, ui);
455                Text::new(&recieving_help_msg)
456                    .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
457                    .font_size(self.fonts.cyri.scale(24))
458                    .font_id(self.fonts.cyri.conrod_id)
459                    .color(HP_COLOR)
460                    .set(state.ids.death_message_1, ui);
461            }
462        }
463    }
464
465    fn show_death_message(&self, state: &State, ui: &mut UiCell) {
466        let localized_strings = self.localized_strings;
467        let hardcore = self.client.current::<Hardcore>().is_some();
468
469        if let Some(key) = self
470            .global_state
471            .settings
472            .controls
473            .get_binding(GameInput::Respawn)
474        {
475            Text::new(&self.localized_strings.get_msg("hud-you_died"))
476                .middle_of(ui.window)
477                .font_size(self.fonts.cyri.scale(50))
478                .font_id(self.fonts.cyri.conrod_id)
479                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
480                .set(state.ids.death_message_1_bg, ui);
481            let respawn_msg = if hardcore {
482                localized_strings.get_msg_ctx(
483                    "hud-press_key_to_return_to_char_menu",
484                    &i18n::fluent_args! {
485                        "key" => key.display_string()
486                    },
487                )
488            } else {
489                localized_strings.get_msg_ctx("hud-press_key_to_respawn", &i18n::fluent_args! {
490                    "key" => key.display_string()
491                })
492            };
493            let penalty_msg = if hardcore {
494                self.localized_strings.get_msg("hud-hardcore_char_deleted")
495            } else {
496                self.localized_strings.get_msg("hud-items_lost_dur")
497            };
498            Text::new(&respawn_msg)
499                .mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0)
500                .font_size(self.fonts.cyri.scale(30))
501                .font_id(self.fonts.cyri.conrod_id)
502                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
503                .set(state.ids.death_message_2_bg, ui);
504            Text::new(&penalty_msg)
505                .mid_bottom_with_margin_on(state.ids.death_message_2_bg, -50.0)
506                .font_size(self.fonts.cyri.scale(30))
507                .font_id(self.fonts.cyri.conrod_id)
508                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
509                .set(state.ids.death_message_3_bg, ui);
510            Text::new(&self.localized_strings.get_msg("hud-you_died"))
511                .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0)
512                .font_size(self.fonts.cyri.scale(50))
513                .font_id(self.fonts.cyri.conrod_id)
514                .color(CRITICAL_HP_COLOR)
515                .set(state.ids.death_message_1, ui);
516            Text::new(&respawn_msg)
517                .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0)
518                .font_size(self.fonts.cyri.scale(30))
519                .font_id(self.fonts.cyri.conrod_id)
520                .color(CRITICAL_HP_COLOR)
521                .set(state.ids.death_message_2, ui);
522            Text::new(&penalty_msg)
523                .bottom_left_with_margins_on(state.ids.death_message_3_bg, 2.0, 2.0)
524                .font_size(self.fonts.cyri.scale(30))
525                .font_id(self.fonts.cyri.conrod_id)
526                .color(CRITICAL_HP_COLOR)
527                .set(state.ids.death_message_3, ui);
528        }
529    }
530
531    fn show_stat_bars(&self, state: &State, ui: &mut UiCell, events: &mut Vec<Event>) {
532        let (hp_percentage, energy_percentage, poise_percentage): (f64, f64, f64) =
533            if self.health.is_dead {
534                (0.0, 0.0, 0.0)
535            } else {
536                let max_hp = f64::from(self.health.base_max().max(self.health.maximum()));
537                let current_hp = f64::from(self.health.current());
538                (
539                    current_hp / max_hp * 100.0,
540                    f64::from(self.energy.fraction() * 100.0),
541                    f64::from(self.poise.fraction() * 100.0),
542                )
543            };
544
545        // Animation timer
546        let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8;
547        let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
548        let bar_values = self.global_state.settings.interface.bar_numbers;
549        let is_downed = is_downed(Some(self.health), self.char_state);
550        let show_health = self.global_state.settings.interface.always_show_bars
551            || is_downed
552            || (self.health.current() - self.health.maximum()).abs() > Health::HEALTH_EPSILON;
553        let show_energy = self.global_state.settings.interface.always_show_bars
554            || (self.energy.current() - self.energy.maximum()).abs() > Energy::ENERGY_EPSILON;
555        let show_poise = self.global_state.settings.interface.enable_poise_bar
556            && (self.global_state.settings.interface.always_show_bars
557                || (self.poise.current() - self.poise.maximum()).abs() > Poise::POISE_EPSILON);
558        let decayed_health = 1.0 - self.health.maximum() as f64 / self.health.base_max() as f64;
559
560        if show_health && !self.health.is_dead || decayed_health > 0.0 {
561            let offset = 1.0;
562            let hp_percentage = if is_downed {
563                100.0
564                    * (1.0 - self.info.key_state.give_up.unwrap_or(0.0) / GIVE_UP_HOLD_TIME)
565                        .clamp(0.0, 1.0) as f64
566            } else {
567                hp_percentage
568            };
569
570            Image::new(self.imgs.health_bg)
571                .w_h(484.0, 24.0)
572                .mid_top_with_margin_on(state.ids.frame, -offset)
573                .set(state.ids.bg_health, ui);
574            Rectangle::fill_with([480.0, 18.0], color::TRANSPARENT)
575                .top_left_with_margins_on(state.ids.bg_health, 2.0, 2.0)
576                .set(state.ids.hp_alignment, ui);
577            let health_col = match hp_percentage as u8 {
578                _ if is_downed => crit_hp_color,
579                0..=20 => crit_hp_color,
580                21..=40 => LOW_HP_COLOR,
581                _ => HP_COLOR,
582            };
583            Image::new(self.imgs.bar_content)
584                .w_h(480.0 * hp_percentage / 100.0, 18.0)
585                .color(Some(health_col))
586                .top_left_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
587                .set(state.ids.hp_filling, ui);
588
589            if decayed_health > 0.0 {
590                let decay_bar_len = 480.0 * decayed_health;
591                Image::new(self.imgs.bar_content)
592                    .w_h(decay_bar_len, 18.0)
593                    .color(Some(QUALITY_EPIC))
594                    .top_right_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
595                    .crop_kids()
596                    .set(state.ids.hp_decayed, ui);
597
598                Image::new(self.imgs.decayed_bg)
599                    .w_h(480.0, 18.0)
600                    .color(Some(Color::Rgba(0.58, 0.29, 0.93, (hp_ani + 0.6).min(1.0))))
601                    .top_left_with_margins_on(state.ids.hp_alignment, 0.0, 0.0)
602                    .parent(state.ids.hp_decayed)
603                    .set(state.ids.decay_overlay, ui);
604            }
605            Image::new(self.imgs.health_frame)
606                .w_h(484.0, 24.0)
607                .color(Some(UI_HIGHLIGHT_0))
608                .middle_of(state.ids.bg_health)
609                .set(state.ids.frame_health, ui);
610        }
611        if show_energy && !self.health.is_dead {
612            let offset = if show_health || decayed_health > 0.0 {
613                33.0
614            } else {
615                1.0
616            };
617            Image::new(self.imgs.energy_bg)
618                .w_h(323.0, 16.0)
619                .mid_top_with_margin_on(state.ids.frame, -offset)
620                .set(state.ids.bg_energy, ui);
621            Rectangle::fill_with([319.0, 10.0], color::TRANSPARENT)
622                .top_left_with_margins_on(state.ids.bg_energy, 2.0, 2.0)
623                .set(state.ids.energy_alignment, ui);
624            Image::new(self.imgs.bar_content)
625                .w_h(319.0 * energy_percentage / 100.0, 10.0)
626                .color(Some(STAMINA_COLOR))
627                .top_left_with_margins_on(state.ids.energy_alignment, 0.0, 0.0)
628                .set(state.ids.energy_filling, ui);
629            Image::new(self.imgs.energy_frame)
630                .w_h(323.0, 16.0)
631                .color(Some(UI_HIGHLIGHT_0))
632                .middle_of(state.ids.bg_energy)
633                .set(state.ids.frame_energy, ui);
634        }
635        if show_poise && !self.health.is_dead {
636            let offset = 16.0;
637
638            let poise_colour = match self.poise.previous_state {
639                self::PoiseState::KnockedDown => BLACK,
640                self::PoiseState::Dazed => Color::Rgba(0.25, 0.0, 0.15, 1.0),
641                self::PoiseState::Stunned => Color::Rgba(0.40, 0.0, 0.30, 1.0),
642                self::PoiseState::Interrupted => Color::Rgba(0.55, 0.0, 0.45, 1.0),
643                _ => POISE_COLOR,
644            };
645
646            Image::new(self.imgs.poise_bg)
647                .w_h(323.0, 14.0)
648                .mid_top_with_margin_on(state.ids.frame, -offset)
649                .set(state.ids.bg_poise, ui);
650            Rectangle::fill_with([319.0, 10.0], color::TRANSPARENT)
651                .top_left_with_margins_on(state.ids.bg_poise, 2.0, 2.0)
652                .set(state.ids.poise_alignment, ui);
653            Image::new(self.imgs.bar_content)
654                .w_h(319.0 * poise_percentage / 100.0, 10.0)
655                .color(Some(poise_colour))
656                .top_left_with_margins_on(state.ids.poise_alignment, 0.0, 0.0)
657                .set(state.ids.poise_filling, ui);
658            for i in 0..state.ids.poise_ticks.len() {
659                Image::new(self.imgs.poise_tick)
660                    .w_h(3.0, 10.0)
661                    .color(Some(POISEBAR_TICK_COLOR))
662                    .top_left_with_margins_on(
663                        state.ids.poise_alignment,
664                        0.0,
665                        319.0f64 * (self::Poise::POISE_THRESHOLDS[i] / self.poise.maximum()) as f64,
666                    )
667                    .set(state.ids.poise_ticks[i], ui);
668            }
669            Image::new(self.imgs.poise_frame)
670                .w_h(323.0, 16.0)
671                .color(Some(UI_HIGHLIGHT_0))
672                .middle_of(state.ids.bg_poise)
673                .set(state.ids.frame_poise, ui);
674        }
675        // Bag button and indicator
676        Image::new(self.imgs.selected_exp_bg)
677            .w_h(34.0, 38.0)
678            .bottom_right_with_margins_on(state.ids.slot10, 0.0, -37.0)
679            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
680            .set(state.ids.bag_img_frame_bg, ui);
681
682        if Button::image(self.imgs.bag_frame)
683            .w_h(34.0, 38.0)
684            .middle_of(state.ids.bag_img_frame_bg)
685            .set(state.ids.bag_img_frame, ui)
686            .was_clicked()
687        {
688            events.push(Event::OpenBag);
689        }
690        let inventory = self.inventory;
691
692        let space_used = inventory.populated_slots();
693        let space_max = inventory.slots().count();
694        let bag_space = format!("{}/{}", space_used, space_max);
695        let bag_space_percentage = space_used as f64 / space_max as f64;
696
697        // bag filling indicator bar
698        Image::new(self.imgs.bar_content)
699            .w_h(1.0, 21.0 * bag_space_percentage)
700            .color(if bag_space_percentage < 0.6 {
701                Some(TEXT_VELORITE)
702            } else if bag_space_percentage < 1.0 {
703                Some(LOW_HP_COLOR)
704            } else {
705                Some(CRITICAL_HP_COLOR)
706            })
707            .graphics_for(state.ids.bag_img_frame)
708            .bottom_left_with_margins_on(state.ids.bag_img_frame, 14.0, 2.0)
709            .set(state.ids.bag_filling, ui);
710
711        // bag filling text
712        Rectangle::fill_with([32.0, 11.0], color::TRANSPARENT)
713            .bottom_left_with_margins_on(state.ids.bag_img_frame_bg, 1.0, 2.0)
714            .graphics_for(state.ids.bag_img_frame)
715            .set(state.ids.bag_numbers_alignment, ui);
716        Text::new(&bag_space)
717            .middle_of(state.ids.bag_numbers_alignment)
718            .font_size(if bag_space.len() < 6 { 9 } else { 8 })
719            .font_id(self.fonts.cyri.conrod_id)
720            .color(BLACK)
721            .graphics_for(state.ids.bag_img_frame)
722            .set(state.ids.bag_space_bg, ui);
723        Text::new(&bag_space)
724            .bottom_right_with_margins_on(state.ids.bag_space_bg, 1.0, 1.0)
725            .font_size(if bag_space.len() < 6 { 9 } else { 8 })
726            .font_id(self.fonts.cyri.conrod_id)
727            .color(if bag_space_percentage < 0.6 {
728                TEXT_VELORITE
729            } else if bag_space_percentage < 1.0 {
730                LOW_HP_COLOR
731            } else {
732                CRITICAL_HP_COLOR
733            })
734            .graphics_for(state.ids.bag_img_frame)
735            .set(state.ids.bag_space, ui);
736
737        Image::new(self.imgs.bag_ico)
738            .w_h(24.0, 24.0)
739            .graphics_for(state.ids.bag_img_frame)
740            .mid_bottom_with_margin_on(state.ids.bag_img_frame, 13.0)
741            .set(state.ids.bag_img, ui);
742
743        if let Some(bag) = &self
744            .global_state
745            .settings
746            .controls
747            .get_binding(GameInput::Inventory)
748        {
749            self.create_new_button_with_shadow(
750                ui,
751                bag,
752                state.ids.bag_img,
753                state.ids.bag_text_bg,
754                state.ids.bag_text,
755            );
756        }
757
758        // Exp Type and Level Display
759
760        // Unspent SP indicator (only show if we can spend SP)
761        let unspent_sp = self.skillset.can_unlock_any_skill();
762        if unspent_sp {
763            let arrow_ani = animation_timer(self.pulse); //Animation timer
764            Image::new(self.imgs.sp_indicator_arrow)
765                .w_h(20.0, 11.0)
766                .graphics_for(state.ids.exp_img_frame)
767                .mid_top_with_margin_on(state.ids.exp_img_frame, -12.0 + arrow_ani as f64)
768                .color(Some(QUALITY_LEGENDARY))
769                .set(state.ids.sp_arrow, ui);
770            Text::new(&self.localized_strings.get_msg("hud-sp_arrow_txt"))
771                .mid_top_with_margin_on(state.ids.sp_arrow, -18.0)
772                .graphics_for(state.ids.exp_img_frame)
773                .font_id(self.fonts.cyri.conrod_id)
774                .font_size(self.fonts.cyri.scale(14))
775                .color(BLACK)
776                .set(state.ids.sp_arrow_txt_bg, ui);
777            Text::new(&self.localized_strings.get_msg("hud-sp_arrow_txt"))
778                .graphics_for(state.ids.exp_img_frame)
779                .bottom_right_with_margins_on(state.ids.sp_arrow_txt_bg, 1.0, 1.0)
780                .font_id(self.fonts.cyri.conrod_id)
781                .font_size(self.fonts.cyri.scale(14))
782                .color(QUALITY_LEGENDARY)
783                .set(state.ids.sp_arrow_txt, ui);
784        }
785
786        if self
787            .global_state
788            .settings
789            .interface
790            .xp_bar_skillgroup
791            .is_some()
792        {
793            let offset = -81.0;
794            let selected_experience = &self
795                .global_state
796                .settings
797                .interface
798                .xp_bar_skillgroup
799                .unwrap_or(SkillGroupKind::General);
800            let current_exp = self.skillset.available_experience(*selected_experience) as f64;
801            let max_exp = self.skillset.skill_point_cost(*selected_experience) as f64;
802            let exp_percentage = current_exp / max_exp.max(1.0);
803            let level = self.skillset.earned_sp(*selected_experience);
804            let level_txt = if level > 0 {
805                self.skillset.earned_sp(*selected_experience).to_string()
806            } else {
807                "".to_string()
808            };
809
810            // Exp Bar
811            Image::new(self.imgs.exp_frame_bg)
812                .w_h(594.0, 8.0)
813                .mid_top_with_margin_on(state.ids.frame, -offset)
814                .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.9)))
815                .set(state.ids.exp_frame_bg, ui);
816            Image::new(self.imgs.exp_frame)
817                .w_h(594.0, 8.0)
818                .middle_of(state.ids.exp_frame_bg)
819                .set(state.ids.exp_frame, ui);
820
821            Image::new(self.imgs.bar_content)
822                .w_h(590.0 * exp_percentage, 4.0)
823                .color(Some(XP_COLOR))
824                .top_left_with_margins_on(state.ids.exp_frame, 2.0, 2.0)
825                .set(state.ids.exp_filling, ui);
826            // Exp Type and Level Display
827            Image::new(self.imgs.selected_exp_bg)
828                .w_h(34.0, 38.0)
829                .top_left_with_margins_on(state.ids.exp_frame, -39.0, 3.0)
830                .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
831                .set(state.ids.exp_img_frame_bg, ui);
832
833            if Button::image(self.imgs.selected_exp)
834                .w_h(34.0, 38.0)
835                .middle_of(state.ids.exp_img_frame_bg)
836                .set(state.ids.exp_img_frame, ui)
837                .was_clicked()
838            {
839                events.push(Event::OpenDiary(*selected_experience));
840            }
841
842            Text::new(&level_txt)
843                .mid_bottom_with_margin_on(state.ids.exp_img_frame, 2.0)
844                .font_size(11)
845                .font_id(self.fonts.cyri.conrod_id)
846                .color(QUALITY_LEGENDARY)
847                .graphics_for(state.ids.exp_img_frame)
848                .set(state.ids.exp_lvl, ui);
849
850            Image::new(match selected_experience {
851                SkillGroupKind::General => self.imgs.swords_crossed,
852                SkillGroupKind::Weapon(ToolKind::Sword) => self.imgs.sword,
853                SkillGroupKind::Weapon(ToolKind::Hammer) => self.imgs.hammer,
854                SkillGroupKind::Weapon(ToolKind::Axe) => self.imgs.axe,
855                SkillGroupKind::Weapon(ToolKind::Sceptre) => self.imgs.sceptre,
856                SkillGroupKind::Weapon(ToolKind::Bow) => self.imgs.bow,
857                SkillGroupKind::Weapon(ToolKind::Staff) => self.imgs.staff,
858                SkillGroupKind::Weapon(ToolKind::Pick) => self.imgs.mining,
859                _ => self.imgs.nothing,
860            })
861            .w_h(24.0, 24.0)
862            .graphics_for(state.ids.exp_img_frame)
863            .mid_bottom_with_margin_on(state.ids.exp_img_frame, 13.0)
864            .set(state.ids.exp_img, ui);
865
866            // Show Shortcut
867            if let Some(diary) = &self
868                .global_state
869                .settings
870                .controls
871                .get_binding(GameInput::Diary)
872            {
873                self.create_new_button_with_shadow(
874                    ui,
875                    diary,
876                    state.ids.exp_img,
877                    state.ids.diary_txt_bg,
878                    state.ids.diary_txt,
879                );
880            }
881        } else {
882            // Only show Spellbook ico
883            Image::new(self.imgs.selected_exp_bg)
884                .w_h(34.0, 38.0)
885                .bottom_left_with_margins_on(state.ids.slot1, 0.0, -37.0)
886                .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
887                .set(state.ids.exp_img_frame_bg, ui);
888
889            if Button::image(self.imgs.selected_exp)
890                .w_h(34.0, 38.0)
891                .middle_of(state.ids.exp_img_frame_bg)
892                .set(state.ids.exp_img_frame, ui)
893                .was_clicked()
894            {
895                events.push(Event::OpenDiary(SkillGroupKind::General));
896            }
897
898            Image::new(self.imgs.spellbook_ico0)
899                .w_h(24.0, 24.0)
900                .graphics_for(state.ids.exp_img_frame)
901                .mid_bottom_with_margin_on(state.ids.exp_img_frame, 13.0)
902                .set(state.ids.exp_img, ui);
903
904            // Show Shortcut
905            if let Some(diary) = &self
906                .global_state
907                .settings
908                .controls
909                .get_binding(GameInput::Diary)
910            {
911                self.create_new_button_with_shadow(
912                    ui,
913                    diary,
914                    state.ids.exp_img,
915                    state.ids.diary_txt_bg,
916                    state.ids.diary_txt,
917                );
918            }
919        }
920
921        // Bar Text
922        let bar_text = if self.health.is_dead {
923            Some((
924                self.localized_strings
925                    .get_msg("hud-group-dead")
926                    .into_owned(),
927                self.localized_strings
928                    .get_msg("hud-group-dead")
929                    .into_owned(),
930                self.localized_strings
931                    .get_msg("hud-group-dead")
932                    .into_owned(),
933            ))
934        } else if let BarNumbers::Values = bar_values {
935            Some((
936                format!(
937                    "{}/{}",
938                    self.health.current().round().max(1.0) as u32, /* Don't show 0 health for
939                                                                    * living players */
940                    self.health.maximum().round() as u32
941                ),
942                format!(
943                    "{}/{}",
944                    self.energy.current().round() as u32,
945                    self.energy.maximum().round() as u32
946                ),
947                String::new(), // Don't obscure the tick mark
948            ))
949        } else if let BarNumbers::Percent = bar_values {
950            Some((
951                format!("{}%", hp_percentage as u32),
952                format!("{}%", energy_percentage as u32),
953                String::new(), // Don't obscure the tick mark
954            ))
955        } else {
956            None
957        };
958        if let Some((hp_txt, energy_txt, poise_txt)) = bar_text {
959            let hp_txt = if is_downed { String::new() } else { hp_txt };
960
961            Text::new(&hp_txt)
962                .middle_of(state.ids.frame_health)
963                .font_size(self.fonts.cyri.scale(12))
964                .font_id(self.fonts.cyri.conrod_id)
965                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
966                .set(state.ids.hp_txt_bg, ui);
967            Text::new(&hp_txt)
968                .bottom_left_with_margins_on(state.ids.hp_txt_bg, 2.0, 2.0)
969                .font_size(self.fonts.cyri.scale(12))
970                .font_id(self.fonts.cyri.conrod_id)
971                .color(TEXT_COLOR)
972                .set(state.ids.hp_txt, ui);
973
974            Text::new(&energy_txt)
975                .middle_of(state.ids.frame_energy)
976                .font_size(self.fonts.cyri.scale(12))
977                .font_id(self.fonts.cyri.conrod_id)
978                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
979                .set(state.ids.energy_txt_bg, ui);
980            Text::new(&energy_txt)
981                .bottom_left_with_margins_on(state.ids.energy_txt_bg, 2.0, 2.0)
982                .font_size(self.fonts.cyri.scale(12))
983                .font_id(self.fonts.cyri.conrod_id)
984                .color(TEXT_COLOR)
985                .set(state.ids.energy_txt, ui);
986
987            Text::new(&poise_txt)
988                .middle_of(state.ids.frame_poise)
989                .font_size(self.fonts.cyri.scale(12))
990                .font_id(self.fonts.cyri.conrod_id)
991                .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
992                .set(state.ids.poise_txt_bg, ui);
993            Text::new(&poise_txt)
994                .bottom_left_with_margins_on(state.ids.poise_txt_bg, 2.0, 2.0)
995                .font_size(self.fonts.cyri.scale(12))
996                .font_id(self.fonts.cyri.conrod_id)
997                .color(TEXT_COLOR)
998                .set(state.ids.poise_txt, ui);
999        }
1000    }
1001
1002    fn show_slotbar(&mut self, state: &State, ui: &mut UiCell, slot_offset: f64) {
1003        let shortcuts = self.global_state.settings.interface.shortcut_numbers;
1004
1005        // TODO: avoid this
1006        let content_source = (
1007            self.hotbar,
1008            self.inventory,
1009            self.energy,
1010            self.skillset,
1011            self.active_abilities,
1012            self.body,
1013            self.combo,
1014            self.char_state,
1015            self.stance,
1016            self.stats,
1017            self.buffs,
1018        );
1019
1020        let image_source = (self.item_imgs, self.imgs);
1021        let mut slot_maker = SlotMaker {
1022            // TODO: is a separate image needed for the frame?
1023            empty_slot: self.imgs.skillbar_slot,
1024            hovered_slot: self.imgs.skillbar_index,
1025            filled_slot: self.imgs.skillbar_slot,
1026            selected_slot: self.imgs.inv_slot_sel,
1027            background_color: None,
1028            content_size: ContentSize {
1029                width_height_ratio: 1.0,
1030                max_fraction: 0.9, /* Changes the item image size by setting a maximum fraction
1031                                    * of either the width or height */
1032            },
1033            selected_content_scale: 1.0,
1034            amount_font: self.fonts.cyri.conrod_id,
1035            amount_margins: Vec2::new(1.0, 1.0),
1036            amount_font_size: self.fonts.cyri.scale(12),
1037            amount_text_color: TEXT_COLOR,
1038            content_source: &content_source,
1039            image_source: &image_source,
1040            slot_manager: Some(self.slot_manager),
1041            last_input: &self.global_state.window.last_input(),
1042            pulse: self.pulse,
1043        };
1044
1045        // Tooltips
1046        let tooltip = Tooltip::new({
1047            // Edge images [t, b, r, l]
1048            // Corner images [tr, tl, br, bl]
1049            let edge = &self.rot_imgs.tt_side;
1050            let corner = &self.rot_imgs.tt_corner;
1051            ImageFrame::new(
1052                [edge.cw180, edge.none, edge.cw270, edge.cw90],
1053                [corner.none, corner.cw270, corner.cw90, corner.cw180],
1054                Color::Rgba(0.08, 0.07, 0.04, 1.0),
1055                5.0,
1056            )
1057        })
1058        .title_font_size(self.fonts.cyri.scale(15))
1059        .parent(ui.window)
1060        .desc_font_size(self.fonts.cyri.scale(12))
1061        .font_id(self.fonts.cyri.conrod_id)
1062        .desc_text_color(TEXT_COLOR);
1063
1064        let item_tooltip = ItemTooltip::new(
1065            {
1066                // Edge images [t, b, r, l]
1067                // Corner images [tr, tl, br, bl]
1068                let edge = &self.rot_imgs.tt_side;
1069                let corner = &self.rot_imgs.tt_corner;
1070                ImageFrame::new(
1071                    [edge.cw180, edge.none, edge.cw270, edge.cw90],
1072                    [corner.none, corner.cw270, corner.cw90, corner.cw180],
1073                    Color::Rgba(0.08, 0.07, 0.04, 1.0),
1074                    5.0,
1075                )
1076            },
1077            self.client,
1078            self.info,
1079            self.imgs,
1080            self.item_imgs,
1081            self.pulse,
1082            self.msm,
1083            self.rbm,
1084            Some(self.inventory),
1085            self.localized_strings,
1086            self.item_i18n,
1087        )
1088        .title_font_size(self.fonts.cyri.scale(20))
1089        .parent(ui.window)
1090        .desc_font_size(self.fonts.cyri.scale(12))
1091        .font_id(self.fonts.cyri.conrod_id)
1092        .desc_text_color(TEXT_COLOR);
1093
1094        let slot_content = |slot| {
1095            let (hotbar, inventory, ..) = content_source;
1096            hotbar.get(slot).and_then(|content| match content {
1097                hotbar::SlotContents::Inventory(i, _) => inventory.get_by_hash(i),
1098                _ => None,
1099            })
1100        };
1101
1102        // Helper
1103        let tooltip_text = |slot| {
1104            let (hotbar, inventory, _, skill_set, active_abilities, _, combo, _, stance, _, buffs) =
1105                content_source;
1106            hotbar.get(slot).and_then(|content| match content {
1107                hotbar::SlotContents::Inventory(i, _) => inventory.get_by_hash(i).map(|item| {
1108                    let (title, desc) =
1109                        util::item_text(item, self.localized_strings, self.item_i18n);
1110
1111                    (title.into(), desc.into())
1112                }),
1113                hotbar::SlotContents::Ability(i) => active_abilities
1114                    .and_then(|a| {
1115                        a.auxiliary_set(Some(inventory), Some(skill_set))
1116                            .get(i)
1117                            .and_then(|a| {
1118                                Ability::from(*a).ability_id(
1119                                    self.char_state,
1120                                    Some(inventory),
1121                                    Some(skill_set),
1122                                    stance,
1123                                    combo,
1124                                    buffs,
1125                                )
1126                            })
1127                    })
1128                    .map(|id| util::ability_description(id, self.localized_strings)),
1129            })
1130        };
1131
1132        slot_maker.empty_slot = self.imgs.skillbar_slot;
1133        slot_maker.selected_slot = self.imgs.skillbar_slot;
1134
1135        let slots = slot_entries(state, slot_offset);
1136        for entry in slots {
1137            let slot = slot_maker
1138                .fabricate(entry.slot, [40.0; 2], false, false)
1139                .filled_slot(self.imgs.skillbar_slot)
1140                .position(entry.position);
1141            // if there is an item attached, show item tooltip
1142            if let Some(item) = slot_content(entry.slot) {
1143                slot.with_item_tooltip(
1144                    self.item_tooltip_manager,
1145                    core::iter::once(item as &dyn ItemDesc),
1146                    &None,
1147                    &item_tooltip,
1148                )
1149                .set(entry.widget_id, ui);
1150            // if we can gather some text to display, show it
1151            } else if let Some((title, desc)) = tooltip_text(entry.slot) {
1152                slot.with_tooltip(self.tooltip_manager, &title, &desc, &tooltip, TEXT_COLOR)
1153                    .set(entry.widget_id, ui);
1154            // if not, just set slot
1155            } else {
1156                slot.set(entry.widget_id, ui);
1157            }
1158
1159            // selection box around current hotbar index
1160            match self.global_state.window.last_input() {
1161                LastInput::Controller => {
1162                    // enable UI if gamepad binding is set for CurrentSlot
1163                    if self
1164                        .global_state
1165                        .settings
1166                        .controller
1167                        .get_game_button_binding(GameInput::CurrentSlot)
1168                        .is_some()
1169                        || self
1170                            .global_state
1171                            .settings
1172                            .controller
1173                            .get_layer_button_binding(GameInput::CurrentSlot)
1174                            .is_some()
1175                    {
1176                        let current_hotbar_selection =
1177                            self.hotbar.currently_selected_slot == entry.slot;
1178                        if current_hotbar_selection {
1179                            let selection_image = self.imgs.skillbar_index;
1180
1181                            Image::new(selection_image)
1182                                .w_h(42.0, 42.0)
1183                                .middle_of(entry.widget_id)
1184                                .graphics_for(entry.widget_id)
1185                                .set(state.ids.slot_highlight, ui);
1186                        }
1187                    }
1188                },
1189                LastInput::Keyboard | LastInput::Mouse => {
1190                    // enable UI if keyboard binding is set for CurrentSlot
1191                    if self
1192                        .global_state
1193                        .settings
1194                        .controls
1195                        .get_binding(GameInput::CurrentSlot)
1196                        .is_some()
1197                    {
1198                        let current_hotbar_selection =
1199                            self.hotbar.currently_selected_slot == entry.slot;
1200                        if current_hotbar_selection {
1201                            let selection_image = self.imgs.skillbar_index;
1202
1203                            Image::new(selection_image)
1204                                .w_h(42.0, 42.0)
1205                                .middle_of(entry.widget_id)
1206                                .graphics_for(entry.widget_id)
1207                                .set(state.ids.slot_highlight, ui);
1208                        }
1209                    }
1210                },
1211            }
1212
1213            // shortcuts
1214            if let ShortcutNumbers::On = shortcuts
1215                && let Some(key) = &self
1216                    .global_state
1217                    .settings
1218                    .controls
1219                    .get_binding(entry.game_input)
1220            {
1221                let position = entry.shortcut_position;
1222                let position_bg = entry.shortcut_position_bg;
1223                let (id, id_bg) = entry.shortcut_widget_ids;
1224
1225                let key_desc = key.display_shortest();
1226                // shortcut text
1227                Text::new(&key_desc)
1228                    .position(position)
1229                    .font_size(self.fonts.cyri.scale(8))
1230                    .font_id(self.fonts.cyri.conrod_id)
1231                    .color(TEXT_COLOR)
1232                    .set(id, ui);
1233                // shortcut background
1234                Text::new(&key_desc)
1235                    .position(position_bg)
1236                    .font_size(self.fonts.cyri.scale(8))
1237                    .font_id(self.fonts.cyri.conrod_id)
1238                    .color(BLACK)
1239                    .set(id_bg, ui);
1240            }
1241        }
1242        // M1 is primary slot on mouse, M2 is primary slot on controller
1243        let (primary_id, primary_bg, secondary_id, secondary_bg) =
1244            match self.global_state.window.last_input() {
1245                LastInput::Keyboard | LastInput::Mouse => (
1246                    state.ids.m1_content,
1247                    state.ids.m1_slot_bg,
1248                    state.ids.m2_content,
1249                    state.ids.m2_slot_bg,
1250                ),
1251                LastInput::Controller => (
1252                    state.ids.m2_content,
1253                    state.ids.m2_slot_bg,
1254                    state.ids.m1_content,
1255                    state.ids.m1_slot_bg,
1256                ),
1257            };
1258
1259        // Slot M1
1260        Image::new(self.imgs.skillbar_slot)
1261            .w_h(40.0, 40.0)
1262            .right_from(state.ids.slot5, slot_offset)
1263            .set(state.ids.m1_slot_bg, ui);
1264
1265        let primary_ability_id = self.active_abilities.and_then(|a| {
1266            Ability::from(a.primary).ability_id(
1267                self.char_state,
1268                Some(self.inventory),
1269                Some(self.skillset),
1270                self.stance,
1271                self.combo,
1272                self.buffs,
1273            )
1274        });
1275
1276        let (primary_ability_title, primary_ability_desc) =
1277            util::ability_description(primary_ability_id.unwrap_or(""), self.localized_strings);
1278
1279        Button::image(
1280            primary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
1281        )
1282        .w_h(36.0, 36.0)
1283        .middle_of(primary_bg)
1284        .with_tooltip(
1285            self.tooltip_manager,
1286            &primary_ability_title,
1287            &primary_ability_desc,
1288            &tooltip,
1289            TEXT_COLOR,
1290        )
1291        .set(primary_id, ui);
1292        // Slot M2
1293        Image::new(self.imgs.skillbar_slot)
1294            .w_h(40.0, 40.0)
1295            .right_from(state.ids.m1_slot_bg, slot_offset)
1296            .set(state.ids.m2_slot_bg, ui);
1297
1298        let secondary_ability_id = self.active_abilities.and_then(|a| {
1299            Ability::from(a.secondary).ability_id(
1300                self.char_state,
1301                Some(self.inventory),
1302                Some(self.skillset),
1303                self.stance,
1304                self.combo,
1305                self.buffs,
1306            )
1307        });
1308
1309        let (secondary_ability_title, secondary_ability_desc) =
1310            util::ability_description(secondary_ability_id.unwrap_or(""), self.localized_strings);
1311
1312        Button::image(
1313            secondary_ability_id.map_or(self.imgs.nothing, |id| util::ability_image(self.imgs, id)),
1314        )
1315        .w_h(36.0, 36.0)
1316        .middle_of(secondary_bg)
1317        .image_color(
1318            if self
1319                .active_abilities
1320                .and_then(|a| {
1321                    a.activate_ability(
1322                        AbilityInput::Secondary,
1323                        Some(self.inventory),
1324                        self.skillset,
1325                        Some(self.body),
1326                        self.char_state,
1327                        self.stance,
1328                        self.combo,
1329                        self.stats,
1330                        self.buffs,
1331                    )
1332                })
1333                .is_some_and(|(a, _, _)| {
1334                    self.energy.current() >= a.energy_cost()
1335                        && self.combo.is_some_and(|c| c.counter() >= a.combo_cost())
1336                        && a.ability_meta()
1337                            .requirements
1338                            .requirements_met(self.stance, Some(self.inventory))
1339                })
1340            {
1341                Color::Rgba(1.0, 1.0, 1.0, 1.0)
1342            } else {
1343                Color::Rgba(0.3, 0.3, 0.3, 0.8)
1344            },
1345        )
1346        .with_tooltip(
1347            self.tooltip_manager,
1348            &secondary_ability_title,
1349            &secondary_ability_desc,
1350            &tooltip,
1351            TEXT_COLOR,
1352        )
1353        .set(secondary_id, ui);
1354
1355        // M1 and M2 icons
1356        match self.global_state.window.last_input() {
1357            LastInput::Keyboard | LastInput::Mouse => {
1358                Image::new(self.imgs.m1_ico)
1359                    .w_h(16.0, 18.0)
1360                    .mid_bottom_with_margin_on(state.ids.m1_content, -11.0)
1361                    .set(state.ids.m1_ico, ui);
1362                Image::new(self.imgs.m2_ico)
1363                    .w_h(16.0, 18.0)
1364                    .mid_bottom_with_margin_on(state.ids.m2_content, -11.0)
1365                    .set(state.ids.m2_ico, ui);
1366            },
1367            LastInput::Controller => {
1368                Image::new(icon_utils::fetch_skillbar_gamepad_left(
1369                    self.global_state.window.controller_type(),
1370                    self.imgs,
1371                ))
1372                .w_h(18.0, 18.0)
1373                .mid_bottom_with_margin_on(state.ids.m1_content, -11.0)
1374                .set(state.ids.m1_ico, ui);
1375                Image::new(icon_utils::fetch_skillbar_gamepad_right(
1376                    self.global_state.window.controller_type(),
1377                    self.imgs,
1378                ))
1379                .w_h(18.0, 18.0)
1380                .mid_bottom_with_margin_on(state.ids.m2_content, -11.0)
1381                .set(state.ids.m2_ico, ui);
1382            },
1383        }
1384    }
1385
1386    fn show_combo_counter(&self, combo_floater: ComboFloater, state: &State, ui: &mut UiCell) {
1387        if combo_floater.combo > 0 {
1388            let combo_txt = format!("{} Combo", combo_floater.combo);
1389            let combo_cnt = combo_floater.combo as f32;
1390            let time_since_last_update = comp::combo::COMBO_DECAY_START - combo_floater.timer;
1391            let alpha = (1.0 - time_since_last_update * 0.2).min(1.0) as f32;
1392            let fnt_col = Color::Rgba(
1393                // White -> Yellow -> Red text color gradient depending on count
1394                (1.0 - combo_cnt / (combo_cnt + 20.0)).max(0.79),
1395                (1.0 - combo_cnt / (combo_cnt + 80.0)).max(0.19),
1396                (1.0 - combo_cnt / (combo_cnt + 5.0)).max(0.17),
1397                alpha,
1398            );
1399            // Increase size for higher counts,
1400            // "flash" on update by increasing the font size by 2.
1401            let fnt_size = ((14.0 + combo_floater.timer as f32 * 0.8).min(30.0)) as u32
1402                + if (time_since_last_update) < 0.1 { 2 } else { 0 };
1403
1404            Rectangle::fill_with([10.0, 10.0], color::TRANSPARENT)
1405                .middle_of(ui.window)
1406                .set(state.ids.combo_align, ui);
1407
1408            Text::new(combo_txt.as_str())
1409                .mid_bottom_with_margin_on(
1410                    state.ids.combo_align,
1411                    -350.0 + time_since_last_update * -8.0,
1412                )
1413                .font_size(self.fonts.cyri.scale(fnt_size))
1414                .font_id(self.fonts.cyri.conrod_id)
1415                .color(Color::Rgba(0.0, 0.0, 0.0, alpha))
1416                .set(state.ids.combo_bg, ui);
1417            Text::new(combo_txt.as_str())
1418                .bottom_right_with_margins_on(state.ids.combo_bg, 1.0, 1.0)
1419                .font_size(self.fonts.cyri.scale(fnt_size))
1420                .font_id(self.fonts.cyri.conrod_id)
1421                .color(fnt_col)
1422                .set(state.ids.combo, ui);
1423        }
1424    }
1425}
1426
1427pub struct State {
1428    ids: Ids,
1429}
1430
1431impl Widget for Skillbar<'_> {
1432    type Event = Vec<Event>;
1433    type State = State;
1434    type Style = ();
1435
1436    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
1437        State {
1438            ids: Ids::new(id_gen),
1439        }
1440    }
1441
1442    fn style(&self) -> Self::Style {}
1443
1444    fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
1445        common_base::prof_span!("Skillbar::update");
1446        let widget::UpdateArgs { state, ui, .. } = args;
1447
1448        let mut events = Vec::new();
1449
1450        let slot_offset = 3.0;
1451
1452        // Death message
1453        if self.health.is_dead {
1454            self.show_death_message(state, ui);
1455        }
1456        // Give up message
1457        else if comp::is_downed(Some(self.health), self.client.current().as_ref()) {
1458            self.show_give_up_message(state, ui);
1459        }
1460
1461        // Skillbar
1462
1463        // Poise bar ticks
1464        state.update(|s| {
1465            s.ids.poise_ticks.resize(
1466                self::Poise::POISE_THRESHOLDS.len(),
1467                &mut ui.widget_id_generator(),
1468            )
1469        });
1470
1471        // Alignment and BG
1472        let alignment_size = 40.0 * 12.0 + slot_offset * 11.0;
1473        Rectangle::fill_with([alignment_size, 80.0], color::TRANSPARENT)
1474            .mid_bottom_with_margin_on(ui.window, 10.0)
1475            .set(state.ids.frame, ui);
1476
1477        // Health, Energy and Poise bars
1478        self.show_stat_bars(state, ui, &mut events);
1479
1480        // Slots
1481        self.show_slotbar(state, ui, slot_offset);
1482
1483        // Combo Counter
1484        if let Some(combo_floater) = self.combo_floater {
1485            self.show_combo_counter(combo_floater, state, ui);
1486        }
1487        events
1488    }
1489}