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