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