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