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