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