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