Skip to main content

veloren_voxygen/hud/
diary.rs

1use super::{
2    BLACK, CRITICAL_HP_COLOR, HP_COLOR, Position, PositionSpecifier, Show, TEXT_COLOR,
3    UI_HIGHLIGHT_0, UI_MAIN, XP_COLOR,
4    img_ids::{Imgs, ImgsRot},
5    item_imgs::{ItemImgs, animate_by_pulse},
6};
7use crate::{
8    GlobalState,
9    game_input::GameInput,
10    hud::{
11        self,
12        slots::{AbilitySlot, SlotManager},
13        util,
14    },
15    ui::{
16        ImageFrame, Tooltip, TooltipManager, Tooltipable,
17        fonts::Fonts,
18        slot::{ContentSize, SlotMaker},
19    },
20};
21use client::{self, Client};
22use common::{
23    combat,
24    comp::{
25        self, Body, Buffs, CharacterState, Combo, Energy, Health, Inventory, Poise, Stance, Stats,
26        ability::{Ability, ActiveAbilities, AuxiliaryAbility, BASE_ABILITY_LIMIT},
27        inventory::{
28            item::{ItemI18n, ItemKind, MaterialStatManifest, item_key::ItemKey, tool::ToolKind},
29            slot::EquipSlot,
30        },
31        skills::{
32            self, AxeSkill, BowSkill, ClimbSkill, HammerSkill, MiningSkill, SKILL_MODIFIERS,
33            SceptreSkill, Skill, SwimSkill, SwordSkill,
34        },
35        skillset::{SkillGroupKind, SkillSet},
36    },
37    resources::BattleMode,
38    uid::Uid,
39};
40use conrod_core::{
41    Color, Colorable, Labelable, Positionable, Sizeable, UiCell, Widget, WidgetCommon, color,
42    image,
43    position::Relative,
44    widget::{self, Button, Image, Rectangle, State, Text},
45    widget_ids,
46};
47use i18n::Localization;
48use std::borrow::Cow;
49use strum::{EnumIter, IntoEnumIterator};
50use vek::*;
51const ART_SIZE: [f64; 2] = [320.0, 320.0];
52
53widget_ids! {
54    pub struct Ids {
55        frame,
56        bg,
57        icon,
58        close,
59        title,
60        content_align,
61        section_imgs[],
62        section_btns[],
63        // Skill tree stuffs
64        exp_bar_bg,
65        exp_bar_frame,
66        exp_bar_content_align,
67        exp_bar_content,
68        exp_bar_rank,
69        exp_bar_txt,
70        active_bar_checkbox,
71        active_bar_checkbox_label,
72        tree_title_txt,
73        lock_imgs[],
74        available_pts_txt,
75        weapon_imgs[],
76        weapon_btns[],
77        skills_top_l_align,
78        skills_top_r_align,
79        skills_bot_l_align,
80        skills_bot_r_align,
81        skills_top_l[],
82        skills_top_r[],
83        skills_bot_l[],
84        skills_bot_r[],
85        skills[],
86        skill_lock_imgs[],
87        sword_bg,
88        sword_stance_cleaving_text,
89        sword_stance_agile_text,
90        sword_stance_crippling_text,
91        sword_stance_heavy_text,
92        sword_stance_defensive_text,
93        sword_stance_cleaving_shadow,
94        sword_stance_agile_shadow,
95        sword_stance_crippling_shadow,
96        sword_stance_heavy_shadow,
97        sword_stance_defensive_shadow,
98        sword_stance_left_align,
99        sword_stance_right_align,
100        axe_bg,
101        hammer_bg,
102        bow_bg,
103        staff_bg,
104        staff_render,
105        skill_staff_basic_0,
106        skill_staff_basic_1,
107        skill_staff_basic_2,
108        skill_staff_basic_3,
109        skill_staff_beam_0,
110        skill_staff_beam_1,
111        skill_staff_beam_2,
112        skill_staff_beam_3,
113        skill_staff_beam_4,
114        skill_staff_shockwave_0,
115        skill_staff_shockwave_1,
116        skill_staff_shockwave_2,
117        skill_staff_shockwave_3,
118        skill_staff_shockwave_4,
119        skill_staff_napalm_strike,
120        skill_staff_flame_cloak,
121        skill_staff_fire_dash,
122        skill_staff_fire_breath,
123        skill_staff_pyroclasm,
124        sceptre_render,
125        skill_sceptre_lifesteal_0,
126        skill_sceptre_lifesteal_1,
127        skill_sceptre_lifesteal_2,
128        skill_sceptre_lifesteal_3,
129        skill_sceptre_lifesteal_4,
130        skill_sceptre_heal_0,
131        skill_sceptre_heal_1,
132        skill_sceptre_heal_2,
133        skill_sceptre_heal_3,
134        skill_sceptre_heal_4,
135        skill_sceptre_aura_0,
136        skill_sceptre_aura_1,
137        skill_sceptre_aura_2,
138        skill_sceptre_aura_3,
139        skill_sceptre_aura_4,
140        pick_render,
141        skill_pick_m1,
142        skill_pick_m1_0,
143        skill_pick_m1_1,
144        skill_pick_m1_2,
145        general_combat_render_0,
146        general_combat_render_1,
147        skill_general_tree_0,
148        skill_general_tree_1,
149        skill_general_tree_2,
150        skill_general_tree_3,
151        skill_general_tree_4,
152        skill_general_tree_5,
153        skill_general_climb_0,
154        skill_general_climb_1,
155        skill_general_climb_2,
156        skill_general_swim_0,
157        skill_general_swim_1,
158        sword_path_overlay,
159        // Ability selection
160        spellbook_art,
161        sb_page_left_align,
162        sb_page_right_align,
163        spellbook_skills_bg,
164        ability_page_left,
165        ability_page_right,
166        active_abilities[],
167        active_abilities_keys[],
168        abilities[],
169        ability_frames[],
170        abilities_dual[],
171        ability_titles[],
172        ability_descs[],
173        // Stats
174        stat_names[],
175        stat_values[],
176        // Recipes
177        recipe_groups[],
178    }
179}
180
181#[derive(WidgetCommon)]
182pub struct Diary<'a> {
183    show: &'a Show,
184    client: &'a Client,
185    global_state: &'a GlobalState,
186    skill_set: &'a SkillSet,
187    active_abilities: &'a ActiveAbilities,
188    inventory: &'a Inventory,
189    char_state: &'a CharacterState,
190    health: &'a Health,
191    energy: &'a Energy,
192    poise: &'a Poise,
193    body: &'a Body,
194    uid: &'a Uid,
195    msm: &'a MaterialStatManifest,
196    imgs: &'a Imgs,
197    item_imgs: &'a ItemImgs,
198    fonts: &'a Fonts,
199    localized_strings: &'a Localization,
200    item_i18n: &'a ItemI18n,
201    rot_imgs: &'a ImgsRot,
202    tooltip_manager: &'a mut TooltipManager,
203    slot_manager: &'a mut SlotManager,
204    pulse: f32,
205    stance: Option<&'a Stance>,
206    combo: Option<&'a Combo>,
207    stats: Option<&'a Stats>,
208    buffs: Option<&'a Buffs>,
209
210    #[conrod(common_builder)]
211    common: widget::CommonBuilder,
212    created_btns_top_l: usize,
213    created_btns_top_r: usize,
214    created_btns_bot_l: usize,
215    created_btns_bot_r: usize,
216}
217
218pub struct DiaryShow {
219    pub skilltreetab: SelectedSkillTree,
220    pub section: DiarySection,
221}
222
223impl Default for DiaryShow {
224    fn default() -> Self {
225        Self {
226            skilltreetab: SelectedSkillTree::General,
227            section: DiarySection::SkillTrees,
228        }
229    }
230}
231
232#[expect(clippy::too_many_arguments)]
233impl<'a> Diary<'a> {
234    pub fn new(
235        show: &'a Show,
236        client: &'a Client,
237        global_state: &'a GlobalState,
238        skill_set: &'a SkillSet,
239        active_abilities: &'a ActiveAbilities,
240        inventory: &'a Inventory,
241        char_state: &'a CharacterState,
242        health: &'a Health,
243        energy: &'a Energy,
244        poise: &'a Poise,
245        body: &'a Body,
246        uid: &'a Uid,
247        msm: &'a MaterialStatManifest,
248        imgs: &'a Imgs,
249        item_imgs: &'a ItemImgs,
250        fonts: &'a Fonts,
251        localized_strings: &'a Localization,
252        item_i18n: &'a ItemI18n,
253        rot_imgs: &'a ImgsRot,
254        tooltip_manager: &'a mut TooltipManager,
255        slot_manager: &'a mut SlotManager,
256        pulse: f32,
257        stance: Option<&'a Stance>,
258        combo: Option<&'a Combo>,
259        stats: Option<&'a Stats>,
260        buffs: Option<&'a Buffs>,
261    ) -> Self {
262        Self {
263            show,
264            client,
265            global_state,
266            skill_set,
267            active_abilities,
268            inventory,
269            char_state,
270            health,
271            energy,
272            poise,
273            body,
274            uid,
275            msm,
276            imgs,
277            item_imgs,
278            fonts,
279            localized_strings,
280            item_i18n,
281            rot_imgs,
282            tooltip_manager,
283            slot_manager,
284            pulse,
285            stance,
286            combo,
287            stats,
288            buffs,
289            common: widget::CommonBuilder::default(),
290            created_btns_top_l: 0,
291            created_btns_top_r: 0,
292            created_btns_bot_l: 0,
293            created_btns_bot_r: 0,
294        }
295    }
296}
297
298pub type SelectedSkillTree = SkillGroupKind;
299
300pub enum Event {
301    Close,
302    ChangeSkillTree(SelectedSkillTree),
303    UnlockSkill(Skill),
304    ChangeSection(DiarySection),
305    SelectExpBar(Option<SkillGroupKind>),
306}
307
308// Possible future sections: Bestiary ("Pokedex" of fought enemies), Weapon and
309// armour catalogue, Achievements...
310#[derive(EnumIter, PartialEq, Eq)]
311pub enum DiarySection {
312    SkillTrees,
313    AbilitySelection,
314    Character,
315    Recipes,
316}
317
318impl DiarySection {
319    fn title_key(&self) -> &'static str {
320        match self {
321            DiarySection::SkillTrees => "hud-diary-sections-skill_trees-title",
322            DiarySection::AbilitySelection => "hud-diary-sections-abilities-title",
323            DiarySection::Character => "hud-diary-sections-character-title",
324            DiarySection::Recipes => "hud-diary-sections-recipes-title",
325        }
326    }
327}
328
329// Represents the SkillGroupKind items
330// that have a skill tree in the diary
331#[derive(EnumIter, PartialEq, Eq)]
332pub enum DiarySkillTree {
333    General,
334    Sword,
335    Axe,
336    Hammer,
337    Bow,
338    Staff,
339    Sceptre,
340    Pick,
341}
342
343impl DiarySkillTree {
344    fn title_key(&self) -> &'static str {
345        match self {
346            DiarySkillTree::General => "hud-skill_tree-general",
347            DiarySkillTree::Sword => "hud-skill_tree-sword",
348            DiarySkillTree::Axe => "hud-skill_tree-axe",
349            DiarySkillTree::Hammer => "hud-skill_tree-hammer",
350            DiarySkillTree::Bow => "hud-skill_tree-bow",
351            DiarySkillTree::Staff => "hud-skill_tree-staff",
352            DiarySkillTree::Sceptre => "hud-skill_tree-sceptre",
353            DiarySkillTree::Pick => "hud-skill_tree-mining",
354        }
355    }
356
357    fn to_skill_group(&self) -> SkillGroupKind {
358        match self {
359            DiarySkillTree::General => SkillGroupKind::General,
360            DiarySkillTree::Sword => SkillGroupKind::Weapon(ToolKind::Sword),
361            DiarySkillTree::Axe => SkillGroupKind::Weapon(ToolKind::Axe),
362            DiarySkillTree::Hammer => SkillGroupKind::Weapon(ToolKind::Hammer),
363            DiarySkillTree::Bow => SkillGroupKind::Weapon(ToolKind::Bow),
364            DiarySkillTree::Staff => SkillGroupKind::Weapon(ToolKind::Staff),
365            DiarySkillTree::Sceptre => SkillGroupKind::Weapon(ToolKind::Sceptre),
366            DiarySkillTree::Pick => SkillGroupKind::Weapon(ToolKind::Pick),
367        }
368    }
369}
370
371pub struct DiaryState {
372    ids: Ids,
373    ability_page: usize,
374    recipe_page: usize,
375}
376
377impl Widget for Diary<'_> {
378    type Event = Vec<Event>;
379    type State = DiaryState;
380    type Style = ();
381
382    fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
383        DiaryState {
384            ids: Ids::new(id_gen),
385            ability_page: 0,
386            recipe_page: 0,
387        }
388    }
389
390    fn style(&self) -> Self::Style {}
391
392    fn update(mut self, args: widget::UpdateArgs<Self>) -> Self::Event {
393        common_base::prof_span!("Diary::update");
394        let widget::UpdateArgs { state, ui, .. } = args;
395        let mut events = Vec::new();
396
397        // Tooltips
398        let diary_tooltip = Tooltip::new({
399            // Edge images [t, b, r, l]
400            // Corner images [tr, tl, br, bl]
401            let edge = &self.rot_imgs.tt_side;
402            let corner = &self.rot_imgs.tt_corner;
403            ImageFrame::new(
404                [edge.cw180, edge.none, edge.cw270, edge.cw90],
405                [corner.none, corner.cw270, corner.cw90, corner.cw180],
406                Color::Rgba(0.08, 0.07, 0.04, 1.0),
407                5.0,
408            )
409        })
410        .title_font_size(self.fonts.cyri.scale(15))
411        .parent(ui.window)
412        .desc_font_size(self.fonts.cyri.scale(12))
413        .font_id(self.fonts.cyri.conrod_id)
414        .desc_text_color(TEXT_COLOR);
415
416        //Animation timer Frame
417        let frame_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8;
418
419        Image::new(self.imgs.diary_bg)
420            .w_h(1202.0, 886.0)
421            .mid_top_with_margin_on(ui.window, 5.0)
422            .color(Some(UI_MAIN))
423            .set(state.ids.bg, ui);
424
425        Image::new(self.imgs.diary_frame)
426            .w_h(1202.0, 886.0)
427            .middle_of(state.ids.bg)
428            .color(Some(UI_HIGHLIGHT_0))
429            .set(state.ids.frame, ui);
430
431        // Icon
432        Image::new(self.imgs.spellbook_button)
433            .w_h(30.0, 27.0)
434            .top_left_with_margins_on(state.ids.frame, 8.0, 8.0)
435            .set(state.ids.icon, ui);
436
437        // X-Button
438        if Button::image(self.imgs.close_btn)
439            .w_h(24.0, 25.0)
440            .hover_image(self.imgs.close_btn_hover)
441            .press_image(self.imgs.close_btn_press)
442            .top_right_with_margins_on(state.ids.frame, 0.0, 0.0)
443            .set(state.ids.close, ui)
444            .was_clicked()
445        {
446            events.push(Event::Close);
447        }
448
449        // Title
450        Text::new(&self.localized_strings.get_msg("hud-diary"))
451            .mid_top_with_margin_on(state.ids.frame, 3.0)
452            .font_id(self.fonts.cyri.conrod_id)
453            .font_size(self.fonts.cyri.scale(29))
454            .color(TEXT_COLOR)
455            .set(state.ids.title, ui);
456
457        // Content Alignment
458        Rectangle::fill_with([599.0 * 2.0, 419.0 * 2.0], color::TRANSPARENT)
459            .mid_top_with_margin_on(state.ids.frame, 46.0)
460            .set(state.ids.content_align, ui);
461
462        // Contents
463        // Section buttons
464        let sel_section = &self.show.diary_fields.section;
465
466        let sections_len = DiarySection::iter().enumerate().len();
467
468        // Update len
469        state.update(|s| {
470            s.ids
471                .section_imgs
472                .resize(sections_len, &mut ui.widget_id_generator())
473        });
474        state.update(|s| {
475            s.ids
476                .section_btns
477                .resize(sections_len, &mut ui.widget_id_generator())
478        });
479
480        for (i, section) in DiarySection::iter().enumerate() {
481            let section_name = self.localized_strings.get_msg(section.title_key());
482
483            let btn_img = {
484                let img = match section {
485                    DiarySection::AbilitySelection => self.imgs.spellbook_ico,
486                    DiarySection::SkillTrees => self.imgs.skilltree_ico,
487                    DiarySection::Character => self.imgs.stats_ico,
488                    DiarySection::Recipes => self.imgs.crafting_ico,
489                };
490
491                if i == 0 {
492                    Image::new(img).top_left_with_margins_on(state.ids.content_align, 0.0, -50.0)
493                } else {
494                    Image::new(img).down_from(state.ids.section_btns[i - 1], 5.0)
495                }
496            };
497            btn_img.w_h(50.0, 50.0).set(state.ids.section_imgs[i], ui);
498            // Section Buttons
499            let border_image = if section == *sel_section {
500                self.imgs.wpn_icon_border_pressed
501            } else {
502                self.imgs.wpn_icon_border
503            };
504
505            let hover_image = if section == *sel_section {
506                self.imgs.wpn_icon_border_pressed
507            } else {
508                self.imgs.wpn_icon_border_mo
509            };
510
511            let press_image = if section == *sel_section {
512                self.imgs.wpn_icon_border_pressed
513            } else {
514                self.imgs.wpn_icon_border_press
515            };
516            let section_buttons = Button::image(border_image)
517                .w_h(50.0, 50.0)
518                .hover_image(hover_image)
519                .press_image(press_image)
520                .middle_of(state.ids.section_imgs[i])
521                .with_tooltip(
522                    self.tooltip_manager,
523                    &section_name,
524                    "",
525                    &diary_tooltip,
526                    TEXT_COLOR,
527                )
528                .set(state.ids.section_btns[i], ui);
529            if section_buttons.was_clicked() {
530                events.push(Event::ChangeSection(section))
531            }
532        }
533        match self.show.diary_fields.section {
534            DiarySection::SkillTrees => {
535                // Skill Trees
536                let sel_tab = &self.show.diary_fields.skilltreetab;
537
538                let skill_trees_len = DiarySkillTree::iter().enumerate().len();
539
540                // Skill Tree Selection
541                state.update(|s| {
542                    s.ids
543                        .weapon_btns
544                        .resize(skill_trees_len, &mut ui.widget_id_generator())
545                });
546                state.update(|s| {
547                    s.ids
548                        .weapon_imgs
549                        .resize(skill_trees_len, &mut ui.widget_id_generator())
550                });
551                state.update(|s| {
552                    s.ids
553                        .lock_imgs
554                        .resize(skill_trees_len, &mut ui.widget_id_generator())
555                });
556
557                // Draw skillgroup tab's icons
558                for (i, skill_tree) in DiarySkillTree::iter().enumerate() {
559                    let skill_tree_name = self.localized_strings.get_msg(skill_tree.title_key());
560                    let skill_group = skill_tree.to_skill_group();
561
562                    // Check if we have this skill tree unlocked
563                    let locked = !self.skill_set.skill_group_accessible(skill_group);
564
565                    // Weapon button image
566                    let btn_img = {
567                        let img = match skill_tree {
568                            DiarySkillTree::General => self.imgs.swords_crossed,
569                            DiarySkillTree::Sword => self.imgs.sword,
570                            DiarySkillTree::Axe => self.imgs.axe,
571                            DiarySkillTree::Hammer => self.imgs.hammer,
572                            DiarySkillTree::Bow => self.imgs.bow,
573                            DiarySkillTree::Staff => self.imgs.staff,
574                            DiarySkillTree::Sceptre => self.imgs.sceptre,
575                            DiarySkillTree::Pick => self.imgs.mining,
576                        };
577
578                        if i == 0 {
579                            Image::new(img).top_left_with_margins_on(
580                                state.ids.content_align,
581                                10.0,
582                                5.0,
583                            )
584                        } else {
585                            Image::new(img).down_from(state.ids.weapon_btns[i - 1], 5.0)
586                        }
587                    };
588                    btn_img.w_h(50.0, 50.0).set(state.ids.weapon_imgs[i], ui);
589
590                    // Lock Image
591                    if locked {
592                        Image::new(self.imgs.lock)
593                            .w_h(50.0, 50.0)
594                            .middle_of(state.ids.weapon_imgs[i])
595                            .graphics_for(state.ids.weapon_imgs[i])
596                            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.8)))
597                            .set(state.ids.lock_imgs[i], ui);
598                    }
599
600                    // Weapon icons
601                    let have_points = {
602                        let available = self.skill_set.available_sp(skill_group);
603
604                        let earned = self.skill_set.earned_sp(skill_group);
605                        let total_cost = skill_group.total_skill_point_cost();
606
607                        available > 0 && (earned - available) < total_cost
608                    };
609
610                    let border_image = if skill_group == *sel_tab || have_points {
611                        self.imgs.wpn_icon_border_pressed
612                    } else {
613                        self.imgs.wpn_icon_border
614                    };
615
616                    let hover_image = if skill_group == *sel_tab {
617                        self.imgs.wpn_icon_border_pressed
618                    } else {
619                        self.imgs.wpn_icon_border_mo
620                    };
621
622                    let press_image = if skill_group == *sel_tab {
623                        self.imgs.wpn_icon_border_pressed
624                    } else {
625                        self.imgs.wpn_icon_border_press
626                    };
627
628                    let color = if skill_group != *sel_tab && have_points {
629                        Color::Rgba(0.92, 0.76, 0.0, frame_ani)
630                    } else {
631                        TEXT_COLOR
632                    };
633
634                    let tooltip_txt = if locked {
635                        self.localized_strings.get_msg("hud-skill-not_unlocked")
636                    } else {
637                        Cow::Borrowed("")
638                    };
639
640                    let wpn_button = Button::image(border_image)
641                        .w_h(50.0, 50.0)
642                        .hover_image(hover_image)
643                        .press_image(press_image)
644                        .middle_of(state.ids.weapon_imgs[i])
645                        .image_color(color)
646                        .with_tooltip(
647                            self.tooltip_manager,
648                            &skill_tree_name,
649                            &tooltip_txt,
650                            &diary_tooltip,
651                            TEXT_COLOR,
652                        )
653                        .set(state.ids.weapon_btns[i], ui);
654                    if wpn_button.was_clicked() {
655                        events.push(Event::ChangeSkillTree(skill_group))
656                    }
657                }
658
659                // Exp Bars and Rank Display
660                let current_exp = self.skill_set.available_experience(*sel_tab) as f64;
661                let max_exp = self.skill_set.skill_point_cost(*sel_tab) as f64;
662                let exp_percentage = current_exp / max_exp;
663                let rank = self.skill_set.earned_sp(*sel_tab);
664                let rank_txt = format!("{}", rank);
665                let exp_txt = format!("{}/{}", current_exp, max_exp);
666                let available_pts = self.skill_set.available_sp(*sel_tab);
667                Image::new(self.imgs.diary_exp_bg)
668                    .w_h(480.0, 76.0)
669                    .mid_bottom_with_margin_on(state.ids.content_align, 10.0)
670                    .set(state.ids.exp_bar_bg, ui);
671                Rectangle::fill_with([400.0, 40.0], color::TRANSPARENT)
672                    .top_left_with_margins_on(state.ids.exp_bar_bg, 32.0, 40.0)
673                    .set(state.ids.exp_bar_content_align, ui);
674                Image::new(self.imgs.bar_content)
675                    .w_h(400.0 * exp_percentage, 40.0)
676                    .top_left_with_margins_on(state.ids.exp_bar_content_align, 0.0, 0.0)
677                    .color(Some(XP_COLOR))
678                    .set(state.ids.exp_bar_content, ui);
679                Image::new(self.imgs.diary_exp_frame)
680                    .w_h(480.0, 76.0)
681                    .color(Some(UI_HIGHLIGHT_0))
682                    .middle_of(state.ids.exp_bar_bg)
683                    .set(state.ids.exp_bar_frame, ui);
684                // Show as Exp bar below skillbar
685                let exp_selected =
686                    self.global_state.settings.interface.xp_bar_skillgroup == Some(*sel_tab);
687                if Button::image(if !exp_selected {
688                    self.imgs.checkbox
689                } else {
690                    self.imgs.checkbox_checked
691                })
692                .w_h(18.0, 18.0)
693                .hover_image(if !exp_selected {
694                    self.imgs.checkbox_mo
695                } else {
696                    self.imgs.checkbox_checked_mo
697                })
698                .press_image(if !exp_selected {
699                    self.imgs.checkbox_press
700                } else {
701                    self.imgs.checkbox_checked
702                })
703                .top_right_with_margins_on(state.ids.exp_bar_frame, 50.0, -30.0)
704                .set(state.ids.active_bar_checkbox, ui)
705                .was_clicked()
706                {
707                    if self.global_state.settings.interface.xp_bar_skillgroup != Some(*sel_tab) {
708                        events.push(Event::SelectExpBar(Some(*sel_tab)));
709                    } else {
710                        events.push(Event::SelectExpBar(None));
711                    }
712                }
713
714                Text::new(&self.localized_strings.get_msg("hud-skill-set_as_exp_bar"))
715                    .right_from(state.ids.active_bar_checkbox, 10.0)
716                    .font_size(self.fonts.cyri.scale(14))
717                    .font_id(self.fonts.cyri.conrod_id)
718                    .graphics_for(state.ids.active_bar_checkbox)
719                    .color(TEXT_COLOR)
720                    .set(state.ids.active_bar_checkbox_label, ui);
721
722                // Show EXP bar text on hover
723                if ui
724                    .widget_input(state.ids.exp_bar_frame)
725                    .mouse()
726                    .is_some_and(|m| m.is_over())
727                {
728                    Text::new(&exp_txt)
729                        .mid_top_with_margin_on(state.ids.exp_bar_frame, 47.0)
730                        .font_id(self.fonts.cyri.conrod_id)
731                        .font_size(self.fonts.cyri.scale(14))
732                        .color(TEXT_COLOR)
733                        .graphics_for(state.ids.exp_bar_frame)
734                        .set(state.ids.exp_bar_txt, ui);
735                }
736                Text::new(&rank_txt)
737                    .mid_top_with_margin_on(state.ids.exp_bar_frame, match rank {
738                        0..=99 => 5.0,
739                        100..=999 => 8.0,
740                        _ => 10.0,
741                    })
742                    .font_id(self.fonts.cyri.conrod_id)
743                    .font_size(self.fonts.cyri.scale(match rank {
744                        0..=99 => 28,
745                        100..=999 => 21,
746                        _ => 15,
747                    }))
748                    .color(TEXT_COLOR)
749                    .set(state.ids.exp_bar_rank, ui);
750
751                Text::new(&self.localized_strings.get_msg_ctx(
752                    "hud-skill-sp_available",
753                    &i18n::fluent_args! {
754                        "number" => available_pts,
755                    },
756                ))
757                .mid_top_with_margin_on(state.ids.content_align, 700.0)
758                .font_id(self.fonts.cyri.conrod_id)
759                .font_size(self.fonts.cyri.scale(28))
760                .color(if available_pts > 0 {
761                    Color::Rgba(0.92, 0.76, 0.0, frame_ani)
762                } else {
763                    TEXT_COLOR
764                })
765                .set(state.ids.available_pts_txt, ui);
766                // Skill Trees
767                // Alignment Placing
768                let x = 200.0;
769                let y = 100.0;
770                // Alignment rectangles for skills
771                Rectangle::fill_with([124.0 * 2.0, 124.0 * 2.0], color::TRANSPARENT)
772                    .top_left_with_margins_on(state.ids.content_align, y, x)
773                    .set(state.ids.skills_top_l_align, ui);
774                Rectangle::fill_with([124.0 * 2.0, 124.0 * 2.0], color::TRANSPARENT)
775                    .top_right_with_margins_on(state.ids.content_align, y, x)
776                    .set(state.ids.skills_top_r_align, ui);
777                Rectangle::fill_with([124.0 * 2.0, 124.0 * 2.0], color::TRANSPARENT)
778                    .bottom_left_with_margins_on(state.ids.content_align, y, x)
779                    .set(state.ids.skills_bot_l_align, ui);
780                Rectangle::fill_with([124.0 * 2.0, 124.0 * 2.0], color::TRANSPARENT)
781                    .bottom_right_with_margins_on(state.ids.content_align, y, x)
782                    .set(state.ids.skills_bot_r_align, ui);
783
784                match sel_tab {
785                    SelectedSkillTree::General => {
786                        self.handle_general_skills_window(&diary_tooltip, state, ui, events)
787                    },
788                    SelectedSkillTree::Weapon(ToolKind::Sword) => {
789                        self.handle_sword_skills_window(&diary_tooltip, state, ui, events)
790                    },
791                    SelectedSkillTree::Weapon(ToolKind::Axe) => {
792                        self.handle_axe_skills_window(&diary_tooltip, state, ui, events)
793                    },
794                    SelectedSkillTree::Weapon(ToolKind::Hammer) => {
795                        self.handle_hammer_skills_window(&diary_tooltip, state, ui, events)
796                    },
797                    SelectedSkillTree::Weapon(ToolKind::Bow) => {
798                        self.handle_bow_skills_window(&diary_tooltip, state, ui, events)
799                    },
800                    SelectedSkillTree::Weapon(ToolKind::Staff) => {
801                        self.handle_staff_skills_window(&diary_tooltip, state, ui, events)
802                    },
803                    SelectedSkillTree::Weapon(ToolKind::Sceptre) => {
804                        self.handle_sceptre_skills_window(&diary_tooltip, state, ui, events)
805                    },
806                    SelectedSkillTree::Weapon(ToolKind::Pick) => {
807                        self.handle_mining_skills_window(&diary_tooltip, state, ui, events)
808                    },
809                    _ => events,
810                }
811            },
812            DiarySection::AbilitySelection => {
813                use comp::ability::AbilityInput;
814
815                // Background Art
816                Image::new(self.imgs.book_bg)
817                    .w_h(299.0 * 4.0, 184.0 * 4.0)
818                    .mid_top_with_margin_on(state.ids.content_align, 4.0)
819                    //.graphics_for(state.ids.content_align)
820                    .set(state.ids.spellbook_art, ui);
821                Image::new(self.imgs.skills_bg)
822                    .w_h(240.0 * 2.0, 40.0 * 2.0)
823                    .mid_bottom_with_margin_on(state.ids.content_align, 8.0)
824                    .set(state.ids.spellbook_skills_bg, ui);
825
826                Rectangle::fill_with([299.0 * 2.0, 184.0 * 4.0], color::TRANSPARENT)
827                    .top_left_with_margins_on(state.ids.spellbook_art, 0.0, 0.0)
828                    .set(state.ids.sb_page_left_align, ui);
829                Rectangle::fill_with([299.0 * 2.0, 184.0 * 4.0], color::TRANSPARENT)
830                    .top_right_with_margins_on(state.ids.spellbook_art, 0.0, 0.0)
831                    .set(state.ids.sb_page_right_align, ui);
832
833                // Display all active abilities on bottom of window
834                state.update(|s| {
835                    s.ids
836                        .active_abilities
837                        .resize(BASE_ABILITY_LIMIT, &mut ui.widget_id_generator())
838                });
839                state.update(|s| {
840                    s.ids
841                        .active_abilities_keys
842                        .resize(BASE_ABILITY_LIMIT, &mut ui.widget_id_generator())
843                });
844
845                let mut slot_maker = SlotMaker {
846                    empty_slot: self.imgs.inv_slot,
847                    hovered_slot: self.imgs.skillbar_index,
848                    filled_slot: self.imgs.inv_slot,
849                    selected_slot: self.imgs.inv_slot_sel,
850                    background_color: Some(UI_MAIN),
851                    content_size: ContentSize {
852                        width_height_ratio: 1.0,
853                        max_fraction: 0.9,
854                    },
855                    selected_content_scale: 1.067,
856                    amount_font: self.fonts.cyri.conrod_id,
857                    amount_margins: Vec2::new(-4.0, 0.0),
858                    amount_font_size: self.fonts.cyri.scale(12),
859                    amount_text_color: TEXT_COLOR,
860                    content_source: &(
861                        self.active_abilities,
862                        self.inventory,
863                        self.skill_set,
864                        self.stance,
865                        self.combo,
866                        Some(self.char_state),
867                        self.stats,
868                        self.buffs,
869                    ),
870                    image_source: self.imgs,
871                    slot_manager: Some(self.slot_manager),
872                    last_input: &self.global_state.window.last_input(),
873                    pulse: 0.0,
874                };
875
876                for i in 0..BASE_ABILITY_LIMIT {
877                    let ability_id = self
878                        .active_abilities
879                        .get_ability(
880                            AbilityInput::Auxiliary(i),
881                            Some(self.inventory),
882                            Some(self.skill_set),
883                            self.stats,
884                        )
885                        .ability_id(
886                            Some(self.char_state),
887                            Some(self.inventory),
888                            Some(self.skill_set),
889                            self.stance,
890                            self.combo,
891                            self.buffs,
892                        );
893                    let (ability_title, ability_desc) = if let Some(ability_id) = ability_id {
894                        util::ability_description(ability_id, self.localized_strings)
895                    } else {
896                        (
897                            Cow::Borrowed("Drag an ability here to use it."),
898                            Cow::Borrowed(""),
899                        )
900                    };
901
902                    let image_size = 80.0;
903                    let image_offsets = 92.0 * i as f64;
904
905                    let slot = AbilitySlot::Slot(i);
906                    let mut ability_slot =
907                        slot_maker.fabricate(slot, [image_size; 2], false, false);
908
909                    if i == 0 {
910                        ability_slot = ability_slot.top_left_with_margins_on(
911                            state.ids.spellbook_skills_bg,
912                            0.0,
913                            32.0 + image_offsets,
914                        );
915                    } else {
916                        ability_slot =
917                            ability_slot.right_from(state.ids.active_abilities[i - 1], 4.0)
918                    }
919                    ability_slot
920                        .with_tooltip(
921                            self.tooltip_manager,
922                            &ability_title,
923                            &ability_desc,
924                            &diary_tooltip,
925                            TEXT_COLOR,
926                        )
927                        .set(state.ids.active_abilities[i], ui);
928
929                    // Display Slot Keybinding
930                    let keys = &self.global_state.settings.controls;
931                    let ability_key = [
932                        GameInput::Slot1,
933                        GameInput::Slot2,
934                        GameInput::Slot3,
935                        GameInput::Slot4,
936                        GameInput::Slot5,
937                    ]
938                    .get(i)
939                    .and_then(|input| keys.get_binding(*input))
940                    .map(|key| key.display_shortest())
941                    .unwrap_or_default();
942
943                    Text::new(&ability_key)
944                        .top_left_with_margins_on(state.ids.active_abilities[i], 0.0, 4.0)
945                        .font_id(self.fonts.cyri.conrod_id)
946                        .font_size(self.fonts.cyri.scale(20))
947                        .color(TEXT_COLOR)
948                        .graphics_for(state.ids.active_abilities[i])
949                        .set(state.ids.active_abilities_keys[i], ui);
950                }
951
952                let abilities: Vec<_> = ActiveAbilities::all_available_abilities(
953                    Some(self.inventory),
954                    Some(self.skill_set),
955                )
956                .into_iter()
957                .map(|a| {
958                    (
959                        Ability::from(a).ability_id(
960                            Some(self.char_state),
961                            Some(self.inventory),
962                            Some(self.skill_set),
963                            self.stance,
964                            self.combo,
965                            self.buffs,
966                        ),
967                        a,
968                    )
969                })
970                .collect();
971
972                const ABILITIES_PER_PAGE: usize = 12;
973
974                let page_indices = (abilities.len().saturating_sub(1)) / ABILITIES_PER_PAGE;
975
976                if state.ability_page > page_indices {
977                    state.update(|s| s.ability_page = 0);
978                }
979
980                state.update(|s| {
981                    s.ids
982                        .abilities
983                        .resize(ABILITIES_PER_PAGE, &mut ui.widget_id_generator())
984                });
985                state.update(|s| {
986                    s.ids
987                        .abilities_dual
988                        .resize(ABILITIES_PER_PAGE, &mut ui.widget_id_generator())
989                });
990                state.update(|s| {
991                    s.ids
992                        .ability_titles
993                        .resize(ABILITIES_PER_PAGE, &mut ui.widget_id_generator())
994                });
995                state.update(|s| {
996                    s.ids
997                        .ability_frames
998                        .resize(ABILITIES_PER_PAGE, &mut ui.widget_id_generator())
999                });
1000                state.update(|s| {
1001                    s.ids
1002                        .ability_descs
1003                        .resize(ABILITIES_PER_PAGE, &mut ui.widget_id_generator())
1004                });
1005
1006                // Page button
1007                // Left Arrow
1008                let left_arrow = Button::image(if state.ability_page > 0 {
1009                    self.imgs.arrow_l
1010                } else {
1011                    self.imgs.arrow_l_inactive
1012                })
1013                .bottom_left_with_margins_on(state.ids.spellbook_art, -83.0, 10.0)
1014                .w_h(48.0, 55.0);
1015                // Grey out arrows when inactive
1016                if state.ability_page > 0 {
1017                    if left_arrow
1018                        .hover_image(self.imgs.arrow_l_click)
1019                        .press_image(self.imgs.arrow_l)
1020                        .set(state.ids.ability_page_left, ui)
1021                        .was_clicked()
1022                    {
1023                        state.update(|s| s.ability_page -= 1);
1024                    }
1025                } else {
1026                    left_arrow.set(state.ids.ability_page_left, ui);
1027                }
1028                // Right Arrow
1029                let right_arrow = Button::image(if state.ability_page < page_indices {
1030                    self.imgs.arrow_r
1031                } else {
1032                    self.imgs.arrow_r_inactive
1033                })
1034                .bottom_right_with_margins_on(state.ids.spellbook_art, -83.0, 10.0)
1035                .w_h(48.0, 55.0);
1036                if state.ability_page < page_indices {
1037                    // Only show right button if not on last page
1038                    if right_arrow
1039                        .hover_image(self.imgs.arrow_r_click)
1040                        .press_image(self.imgs.arrow_r)
1041                        .set(state.ids.ability_page_right, ui)
1042                        .was_clicked()
1043                    {
1044                        state.update(|s| s.ability_page += 1);
1045                    };
1046                } else {
1047                    right_arrow.set(state.ids.ability_page_right, ui);
1048                }
1049
1050                let ability_start = state.ability_page * ABILITIES_PER_PAGE;
1051
1052                let mut slot_maker = SlotMaker {
1053                    empty_slot: self.imgs.inv_slot,
1054                    hovered_slot: self.imgs.skillbar_index,
1055                    filled_slot: self.imgs.inv_slot,
1056                    selected_slot: self.imgs.inv_slot_sel,
1057                    background_color: Some(UI_MAIN),
1058                    content_size: ContentSize {
1059                        width_height_ratio: 1.0,
1060                        max_fraction: 1.0,
1061                    },
1062                    selected_content_scale: 1.067,
1063                    amount_font: self.fonts.cyri.conrod_id,
1064                    amount_margins: Vec2::new(-4.0, 0.0),
1065                    amount_font_size: self.fonts.cyri.scale(12),
1066                    amount_text_color: TEXT_COLOR,
1067                    content_source: &(
1068                        self.active_abilities,
1069                        self.inventory,
1070                        self.skill_set,
1071                        self.stance,
1072                        self.combo,
1073                        Some(self.char_state),
1074                        self.stats,
1075                        self.buffs,
1076                    ),
1077                    image_source: self.imgs,
1078                    slot_manager: Some(self.slot_manager),
1079                    last_input: &self.global_state.window.last_input(),
1080                    pulse: 0.0,
1081                };
1082
1083                let same_weap_kinds = self
1084                    .inventory
1085                    .equipped(EquipSlot::ActiveMainhand)
1086                    .zip(self.inventory.equipped(EquipSlot::ActiveOffhand))
1087                    .is_some_and(|(a, b)| {
1088                        if let (ItemKind::Tool(tool_a), ItemKind::Tool(tool_b)) =
1089                            (&*a.kind(), &*b.kind())
1090                        {
1091                            (a.ability_spec(), tool_a.kind) == (b.ability_spec(), tool_b.kind)
1092                        } else {
1093                            false
1094                        }
1095                    });
1096
1097                for (id_index, (ability_id, ability)) in abilities
1098                    .iter()
1099                    .skip(ability_start)
1100                    .take(ABILITIES_PER_PAGE)
1101                    .enumerate()
1102                {
1103                    let (ability_title, ability_desc) =
1104                        util::ability_description(ability_id.unwrap_or(""), self.localized_strings);
1105
1106                    let (align_state, image_offsets) = if id_index < 6 {
1107                        (state.ids.sb_page_left_align, 120.0 * id_index as f64)
1108                    } else {
1109                        (state.ids.sb_page_right_align, 120.0 * (id_index - 6) as f64)
1110                    };
1111
1112                    Image::new(if same_weap_kinds {
1113                        self.imgs.ability_frame_dual
1114                    } else {
1115                        self.imgs.ability_frame
1116                    })
1117                    .w_h(566.0, 108.0)
1118                    .top_left_with_margins_on(align_state, 16.0 + image_offsets, 16.0)
1119                    .color(Some(UI_HIGHLIGHT_0))
1120                    .set(state.ids.ability_frames[id_index], ui);
1121
1122                    let slot = AbilitySlot::Ability(*ability);
1123                    slot_maker
1124                        .fabricate(slot, [100.0; 2], false, false)
1125                        .top_left_with_margins_on(align_state, 20.0 + image_offsets, 20.0)
1126                        .set(state.ids.abilities[id_index], ui);
1127
1128                    if same_weap_kinds && let AuxiliaryAbility::MainWeapon(slot) = ability {
1129                        let ability = AuxiliaryAbility::OffWeapon(*slot);
1130
1131                        let slot = AbilitySlot::Ability(ability);
1132                        slot_maker
1133                            .fabricate(slot, [100.0; 2], false, false)
1134                            .top_right_with_margins_on(align_state, 20.0 + image_offsets, 20.0)
1135                            .set(state.ids.abilities_dual[id_index], ui);
1136                    }
1137                    // The page width...
1138                    let text_width = 299.0 * 2.0
1139                        - if same_weap_kinds && matches!(ability, AuxiliaryAbility::MainWeapon(_)) {
1140                            // with double the width of an ability image and some padding subtracted
1141                            // if dual wielding two of the same weapon kind
1142                            (20.0 + 100.0 + 10.0) * 2.0
1143                        } else {
1144                            // or the width of an ability image and some padding subtracted
1145                            // otherwise
1146                            20.0 * 2.0 + 100.0
1147                        };
1148                    Text::new(&ability_title)
1149                        .top_left_with_margins_on(state.ids.abilities[id_index], 5.0, 110.0)
1150                        .font_id(self.fonts.cyri.conrod_id)
1151                        .font_size(self.fonts.cyri.scale(28))
1152                        .color(TEXT_COLOR)
1153                        .w(text_width)
1154                        .graphics_for(state.ids.abilities[id_index])
1155                        .set(state.ids.ability_titles[id_index], ui);
1156                    Text::new(&ability_desc)
1157                        .top_left_with_margins_on(state.ids.abilities[id_index], 40.0, 110.0)
1158                        .font_id(self.fonts.cyri.conrod_id)
1159                        .font_size(self.fonts.cyri.scale(13))
1160                        .color(TEXT_COLOR)
1161                        .w(text_width)
1162                        .graphics_for(state.ids.abilities[id_index])
1163                        .set(state.ids.ability_descs[id_index], ui);
1164                }
1165
1166                events
1167            },
1168            DiarySection::Character => {
1169                // Background Art
1170                Image::new(self.imgs.book_bg)
1171                    .w_h(299.0 * 4.0, 184.0 * 4.0)
1172                    .mid_top_with_margin_on(state.ids.content_align, 4.0)
1173                    .set(state.ids.spellbook_art, ui);
1174
1175                if state.ids.stat_names.len() < STAT_COUNT {
1176                    state.update(|s| {
1177                        s.ids
1178                            .stat_names
1179                            .resize(STAT_COUNT, &mut ui.widget_id_generator());
1180                        s.ids
1181                            .stat_values
1182                            .resize(STAT_COUNT, &mut ui.widget_id_generator());
1183                    });
1184                }
1185
1186                for (i, stat) in CharacterStat::iter().enumerate() {
1187                    // Stat names
1188                    let localized_name = stat.localized_str(self.localized_strings);
1189                    let mut txt = Text::new(&localized_name)
1190                        .font_id(self.fonts.cyri.conrod_id)
1191                        .font_size(self.fonts.cyri.scale(29))
1192                        .color(BLACK);
1193
1194                    if i == 0 {
1195                        txt = txt.top_left_with_margins_on(state.ids.spellbook_art, 20.0, 20.0);
1196                    } else {
1197                        txt = txt.down_from(state.ids.stat_names[i - 1], 10.0);
1198                    };
1199                    txt.set(state.ids.stat_names[i], ui);
1200
1201                    let main_weap_stats = self
1202                        .inventory
1203                        .equipped(EquipSlot::ActiveMainhand)
1204                        .and_then(|item| match &*item.kind() {
1205                            ItemKind::Tool(tool) => {
1206                                Some(tool.stats(item.stats_durability_multiplier()))
1207                            },
1208                            _ => None,
1209                        });
1210
1211                    let off_weap_stats = self
1212                        .inventory
1213                        .equipped(EquipSlot::ActiveOffhand)
1214                        .and_then(|item| match &*item.kind() {
1215                            ItemKind::Tool(tool) => {
1216                                Some(tool.stats(item.stats_durability_multiplier()))
1217                            },
1218                            _ => None,
1219                        });
1220
1221                    let (name, _gender, battle_mode) = self
1222                        .client
1223                        .player_list()
1224                        .get(self.uid)
1225                        .and_then(|info| info.character.as_ref())
1226                        .map_or_else(
1227                            || ("Unknown".to_string(), None, BattleMode::PvP),
1228                            |character_info| {
1229                                (
1230                                    self.localized_strings.get_content(&character_info.name),
1231                                    character_info.gender,
1232                                    character_info.battle_mode,
1233                                )
1234                            },
1235                        );
1236
1237                    // Stat values
1238                    let value = match stat {
1239                        CharacterStat::Name => name,
1240                        CharacterStat::BattleMode => match battle_mode {
1241                            BattleMode::PvP => "PvP".to_string(),
1242                            BattleMode::PvE => "PvE".to_string(),
1243                        },
1244                        CharacterStat::Waypoint => self
1245                            .client
1246                            .waypoint()
1247                            .as_ref()
1248                            .cloned()
1249                            .unwrap_or_else(|| "Unknown".to_string()),
1250                        CharacterStat::Hitpoints => format!("{}", self.health.maximum() as u32),
1251                        CharacterStat::Energy => format!("{}", self.energy.maximum() as u32),
1252                        CharacterStat::Poise => format!("{}", self.poise.maximum() as u32),
1253                        CharacterStat::CombatRating => {
1254                            let cr = combat::combat_rating(
1255                                self.inventory,
1256                                self.health,
1257                                self.energy,
1258                                self.poise,
1259                                self.skill_set,
1260                                *self.body,
1261                                self.msm,
1262                            );
1263                            format!("{:.2}", cr * 10.0)
1264                        },
1265                        CharacterStat::Protection => {
1266                            let protection =
1267                                combat::compute_protection(Some(self.inventory), self.msm);
1268                            match protection {
1269                                Some(prot) => format!("{}", prot),
1270                                None => String::from("Invincible"),
1271                            }
1272                        },
1273                        CharacterStat::StunResistance => {
1274                            let stun_res = Poise::compute_poise_damage_reduction(
1275                                Some(self.inventory),
1276                                self.msm,
1277                                None,
1278                                self.stats,
1279                            );
1280                            format!("{:.2}%", stun_res * 100.0)
1281                        },
1282                        CharacterStat::PrecisionPower => {
1283                            let precision_power =
1284                                combat::compute_precision_mult(Some(self.inventory), self.msm);
1285                            format!("x{:.2}", precision_power)
1286                        },
1287                        CharacterStat::EnergyReward => {
1288                            let energy_rew =
1289                                combat::compute_energy_reward_mod(Some(self.inventory), self.msm);
1290                            format!("{:+.0}%", (energy_rew - 1.0) * 100.0)
1291                        },
1292                        CharacterStat::Stealth => {
1293                            let stealth_perception_multiplier =
1294                                combat::perception_dist_multiplier_from_stealth(
1295                                    Some(self.inventory),
1296                                    None,
1297                                    self.msm,
1298                                );
1299                            let txt =
1300                                format!("{:+.1}%", (1.0 - stealth_perception_multiplier) * 100.0);
1301
1302                            txt
1303                        },
1304                        CharacterStat::WeaponPower => match (main_weap_stats, off_weap_stats) {
1305                            (Some(m_stats), Some(o_stats)) => {
1306                                format!("{}   {}", m_stats.power * 10.0, o_stats.power * 10.0)
1307                            },
1308                            (Some(stats), None) | (None, Some(stats)) => {
1309                                format!("{}", stats.power * 10.0)
1310                            },
1311                            (None, None) => String::new(),
1312                        },
1313                        CharacterStat::WeaponSpeed => {
1314                            let spd_fmt = |sp| (sp - 1.0) * 100.0;
1315                            match (main_weap_stats, off_weap_stats) {
1316                                (Some(m_stats), Some(o_stats)) => format!(
1317                                    "{:+.0}%   {:+.0}%",
1318                                    spd_fmt(m_stats.speed),
1319                                    spd_fmt(o_stats.speed)
1320                                ),
1321                                (Some(stats), None) | (None, Some(stats)) => {
1322                                    format!("{:+.0}%", spd_fmt(stats.speed))
1323                                },
1324                                _ => String::new(),
1325                            }
1326                        },
1327                        CharacterStat::WeaponEffectPower => match (main_weap_stats, off_weap_stats)
1328                        {
1329                            (Some(m_stats), Some(o_stats)) => {
1330                                format!(
1331                                    "{}   {}",
1332                                    m_stats.effect_power * 10.0,
1333                                    o_stats.effect_power * 10.0
1334                                )
1335                            },
1336                            (Some(stats), None) | (None, Some(stats)) => {
1337                                format!("{}", stats.effect_power * 10.0)
1338                            },
1339                            (None, None) => String::new(),
1340                        },
1341                    };
1342
1343                    let mut number = Text::new(&value)
1344                        .font_id(self.fonts.cyri.conrod_id)
1345                        .font_size(self.fonts.cyri.scale(29))
1346                        .color(BLACK);
1347
1348                    if i == 0 {
1349                        number = number.right_from(state.ids.stat_names[i], 165.0);
1350                    } else {
1351                        number = number.down_from(state.ids.stat_values[i - 1], 10.0);
1352                    };
1353                    number.set(state.ids.stat_values[i], ui);
1354                }
1355
1356                events
1357            },
1358            DiarySection::Recipes => {
1359                // Background Art
1360                Image::new(self.imgs.book_bg)
1361                    .w_h(299.0 * 4.0, 184.0 * 4.0)
1362                    .mid_top_with_margin_on(state.ids.content_align, 4.0)
1363                    .set(state.ids.spellbook_art, ui);
1364
1365                Rectangle::fill_with([299.0 * 2.0, 184.0 * 4.0], color::TRANSPARENT)
1366                    .top_left_with_margins_on(state.ids.spellbook_art, 0.0, 0.0)
1367                    .set(state.ids.sb_page_left_align, ui);
1368                Rectangle::fill_with([299.0 * 2.0, 184.0 * 4.0], color::TRANSPARENT)
1369                    .top_right_with_margins_on(state.ids.spellbook_art, 0.0, 0.0)
1370                    .set(state.ids.sb_page_right_align, ui);
1371
1372                const RECIPES_PER_PAGE: usize = 36;
1373
1374                let page_index_max =
1375                    self.inventory.recipe_groups_iter().len().saturating_sub(1) / RECIPES_PER_PAGE;
1376
1377                if state.recipe_page > page_index_max {
1378                    state.update(|s| s.recipe_page = 0);
1379                }
1380
1381                // Page button
1382                // Left Arrow
1383                let left_arrow = Button::image(if state.recipe_page > 0 {
1384                    self.imgs.arrow_l
1385                } else {
1386                    self.imgs.arrow_l_inactive
1387                })
1388                .bottom_left_with_margins_on(state.ids.spellbook_art, -83.0, 10.0)
1389                .w_h(48.0, 55.0);
1390                // Grey out arrows when inactive
1391                if state.recipe_page > 0 {
1392                    if left_arrow
1393                        .hover_image(self.imgs.arrow_l_click)
1394                        .press_image(self.imgs.arrow_l)
1395                        .set(state.ids.ability_page_left, ui)
1396                        .was_clicked()
1397                    {
1398                        state.update(|s| s.recipe_page -= 1);
1399                    }
1400                } else {
1401                    left_arrow.set(state.ids.ability_page_left, ui);
1402                }
1403                // Right Arrow
1404                let right_arrow = Button::image(if state.recipe_page < page_index_max {
1405                    self.imgs.arrow_r
1406                } else {
1407                    self.imgs.arrow_r_inactive
1408                })
1409                .bottom_right_with_margins_on(state.ids.spellbook_art, -83.0, 10.0)
1410                .w_h(48.0, 55.0);
1411                if state.recipe_page < page_index_max {
1412                    // Only show right button if not on last page
1413                    if right_arrow
1414                        .hover_image(self.imgs.arrow_r_click)
1415                        .press_image(self.imgs.arrow_r)
1416                        .set(state.ids.ability_page_right, ui)
1417                        .was_clicked()
1418                    {
1419                        state.update(|s| s.recipe_page += 1);
1420                    };
1421                } else {
1422                    right_arrow.set(state.ids.ability_page_right, ui);
1423                }
1424
1425                state.update(|s| {
1426                    s.ids
1427                        .recipe_groups
1428                        .resize(RECIPES_PER_PAGE, &mut ui.widget_id_generator())
1429                });
1430
1431                for (i, rg) in self
1432                    .inventory
1433                    .recipe_groups_iter()
1434                    .skip(state.recipe_page * RECIPES_PER_PAGE)
1435                    .take(RECIPES_PER_PAGE)
1436                    .enumerate()
1437                {
1438                    let (title, _desc) =
1439                        util::item_text(rg, self.localized_strings, self.item_i18n);
1440
1441                    let mut text = Text::new(&title)
1442                        .font_id(self.fonts.cyri.conrod_id)
1443                        .font_size(self.fonts.cyri.scale(29))
1444                        .color(BLACK);
1445
1446                    if i == 0 {
1447                        text =
1448                            text.top_left_with_margins_on(state.ids.sb_page_left_align, 20.0, 20.0);
1449                    } else if i == 18 {
1450                        text = text.top_left_with_margins_on(
1451                            state.ids.sb_page_right_align,
1452                            20.0,
1453                            20.0,
1454                        );
1455                    } else {
1456                        text = text.down_from(state.ids.recipe_groups[i - 1], 10.0);
1457                    }
1458                    text.set(state.ids.recipe_groups[i], ui);
1459                }
1460
1461                events
1462            },
1463        }
1464    }
1465}
1466
1467enum SkillIcon<'a> {
1468    Unlockable {
1469        skill: Skill,
1470        image: image::Id,
1471        position: PositionSpecifier,
1472        id: widget::Id,
1473    },
1474    Descriptive {
1475        title: &'a str,
1476        desc: &'a str,
1477        image: image::Id,
1478        position: PositionSpecifier,
1479        id: widget::Id,
1480    },
1481    Ability {
1482        skill: Skill,
1483        ability_id: &'a str,
1484        position: PositionSpecifier,
1485    },
1486}
1487
1488impl Diary<'_> {
1489    fn handle_general_skills_window(
1490        &mut self,
1491        diary_tooltip: &Tooltip,
1492        state: &mut State<DiaryState>,
1493        ui: &mut UiCell,
1494        mut events: Vec<Event>,
1495    ) -> Vec<Event> {
1496        let tree_title = &self.localized_strings.get_msg("common-weapons-general");
1497        Text::new(tree_title)
1498            .mid_top_with_margin_on(state.ids.content_align, 2.0)
1499            .font_id(self.fonts.cyri.conrod_id)
1500            .font_size(self.fonts.cyri.scale(34))
1501            .color(TEXT_COLOR)
1502            .set(state.ids.tree_title_txt, ui);
1503
1504        // Number of skills per rectangle per weapon, start counting at 0
1505        // Maximum of 9 skills/8 indices
1506        let skills_top_l = 6;
1507        let skills_top_r = 0;
1508        let skills_bot_l = 0;
1509        let skills_bot_r = 5;
1510
1511        self.setup_state_for_skill_icons(
1512            state,
1513            ui,
1514            skills_top_l,
1515            skills_top_r,
1516            skills_bot_l,
1517            skills_bot_r,
1518        );
1519
1520        use SkillGroupKind::*;
1521        use ToolKind::*;
1522        // General Combat
1523        Image::new(animate_by_pulse(
1524            &self.item_imgs.img_ids_or_not_found_img(ItemKey::Simple(
1525                "example_general_combat_left".to_string(),
1526            )),
1527            self.pulse,
1528        ))
1529        .wh(ART_SIZE)
1530        .middle_of(state.ids.content_align)
1531        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1532        .set(state.ids.general_combat_render_0, ui);
1533
1534        Image::new(animate_by_pulse(
1535            &self.item_imgs.img_ids_or_not_found_img(ItemKey::Simple(
1536                "example_general_combat_right".to_string(),
1537            )),
1538            self.pulse,
1539        ))
1540        .wh(ART_SIZE)
1541        .middle_of(state.ids.general_combat_render_0)
1542        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1543        .set(state.ids.general_combat_render_1, ui);
1544
1545        use PositionSpecifier::MidTopWithMarginOn;
1546        let skill_buttons = &[
1547            // Top Left skills
1548            //        5 1 6
1549            //        3 0 4
1550            //        8 2 7
1551            // Bottom left skills
1552            SkillIcon::Unlockable {
1553                skill: Skill::UnlockGroup(Weapon(Sword)),
1554                image: self.imgs.unlock_sword_skill,
1555                position: MidTopWithMarginOn(state.ids.skills_top_l[0], 3.0),
1556                id: state.ids.skill_general_tree_0,
1557            },
1558            SkillIcon::Unlockable {
1559                skill: Skill::UnlockGroup(Weapon(Axe)),
1560                image: self.imgs.unlock_axe_skill,
1561                position: MidTopWithMarginOn(state.ids.skills_top_l[1], 3.0),
1562                id: state.ids.skill_general_tree_1,
1563            },
1564            SkillIcon::Unlockable {
1565                skill: Skill::UnlockGroup(Weapon(Hammer)),
1566                image: self.imgs.unlock_hammer_skill,
1567                position: MidTopWithMarginOn(state.ids.skills_top_l[2], 3.0),
1568                id: state.ids.skill_general_tree_2,
1569            },
1570            SkillIcon::Unlockable {
1571                skill: Skill::UnlockGroup(Weapon(Bow)),
1572                image: self.imgs.unlock_bow_skill,
1573                position: MidTopWithMarginOn(state.ids.skills_top_l[3], 3.0),
1574                id: state.ids.skill_general_tree_3,
1575            },
1576            SkillIcon::Unlockable {
1577                skill: Skill::UnlockGroup(Weapon(Staff)),
1578                image: self.imgs.unlock_staff_skill0,
1579                position: MidTopWithMarginOn(state.ids.skills_top_l[4], 3.0),
1580                id: state.ids.skill_general_tree_4,
1581            },
1582            SkillIcon::Unlockable {
1583                skill: Skill::UnlockGroup(Weapon(Sceptre)),
1584                image: self.imgs.unlock_sceptre_skill,
1585                position: MidTopWithMarginOn(state.ids.skills_top_l[5], 3.0),
1586                id: state.ids.skill_general_tree_5,
1587            },
1588            // Bottom right skills
1589            SkillIcon::Descriptive {
1590                title: "hud-skill-climbing_title",
1591                desc: "hud-skill-climbing",
1592                image: self.imgs.skill_climbing_skill,
1593                position: MidTopWithMarginOn(state.ids.skills_bot_r[0], 3.0),
1594                id: state.ids.skill_general_climb_0,
1595            },
1596            SkillIcon::Unlockable {
1597                skill: Skill::Climb(ClimbSkill::Cost),
1598                image: self.imgs.utility_cost_skill,
1599                position: MidTopWithMarginOn(state.ids.skills_bot_r[1], 3.0),
1600                id: state.ids.skill_general_climb_1,
1601            },
1602            SkillIcon::Unlockable {
1603                skill: Skill::Climb(ClimbSkill::Speed),
1604                image: self.imgs.utility_speed_skill,
1605                position: MidTopWithMarginOn(state.ids.skills_bot_r[2], 3.0),
1606                id: state.ids.skill_general_climb_2,
1607            },
1608            SkillIcon::Descriptive {
1609                title: "hud-skill-swim_title",
1610                desc: "hud-skill-swim",
1611                image: self.imgs.skill_swim_skill,
1612                position: MidTopWithMarginOn(state.ids.skills_bot_r[3], 3.0),
1613                id: state.ids.skill_general_swim_0,
1614            },
1615            SkillIcon::Unlockable {
1616                skill: Skill::Swim(SwimSkill::Speed),
1617                image: self.imgs.utility_speed_skill,
1618                position: MidTopWithMarginOn(state.ids.skills_bot_r[4], 3.0),
1619                id: state.ids.skill_general_swim_1,
1620            },
1621        ];
1622
1623        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
1624        events
1625    }
1626
1627    fn handle_sword_skills_window(
1628        &mut self,
1629        diary_tooltip: &Tooltip,
1630        state: &mut State<DiaryState>,
1631        ui: &mut UiCell,
1632        mut events: Vec<Event>,
1633    ) -> Vec<Event> {
1634        Image::new(self.imgs.sword_tree_paths)
1635            .wh([1042.0, 636.0])
1636            .mid_top_with_margin_on(state.ids.content_align, 55.0)
1637            .graphics_for(state.ids.content_align)
1638            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1639            .set(state.ids.sword_path_overlay, ui);
1640
1641        // Sword
1642        Image::new(self.imgs.sword_bg)
1643            .wh([933.0, 615.0])
1644            .mid_top_with_margin_on(state.ids.content_align, 65.0)
1645            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1646            .set(state.ids.sword_bg, ui);
1647
1648        // Stances:
1649        // For alignment purposes
1650        Rectangle::fill_with([169.0, 615.0], color::TRANSPARENT)
1651            .top_left_of(state.ids.sword_bg)
1652            .set(state.ids.sword_stance_left_align, ui);
1653        Rectangle::fill_with([169.0, 615.0], color::TRANSPARENT)
1654            .top_right_of(state.ids.sword_bg)
1655            .set(state.ids.sword_stance_right_align, ui);
1656
1657        // Cleaving
1658        Text::new(
1659            &self
1660                .localized_strings
1661                .get_msg("hud-skill-sword_stance_cleaving"),
1662        )
1663        .mid_top_with_margin_on(state.ids.sword_stance_left_align, -7.0)
1664        .font_id(self.fonts.cyri.conrod_id)
1665        .font_size(self.fonts.cyri.scale(34))
1666        .color(Color::Rgba(0.94, 0.54, 0.07, 1.0))
1667        .set(state.ids.sword_stance_cleaving_text, ui);
1668
1669        Text::new(
1670            &self
1671                .localized_strings
1672                .get_msg("hud-skill-sword_stance_cleaving"),
1673        )
1674        .x_y_position_relative_to(
1675            state.ids.sword_stance_cleaving_text,
1676            Relative::Scalar(2.5),
1677            Relative::Scalar(-2.5),
1678        )
1679        .depth(1.0)
1680        .font_id(self.fonts.cyri.conrod_id)
1681        .font_size(self.fonts.cyri.scale(34))
1682        .color(color::BLACK)
1683        .set(state.ids.sword_stance_cleaving_shadow, ui);
1684
1685        // Agile
1686        Text::new(
1687            &self
1688                .localized_strings
1689                .get_msg("hud-skill-sword_stance_agile"),
1690        )
1691        .mid_top_with_margin_on(state.ids.sword_bg, -7.0)
1692        .font_id(self.fonts.cyri.conrod_id)
1693        .font_size(self.fonts.cyri.scale(34))
1694        .color(Color::Rgba(0.81, 0.70, 0.08, 1.0))
1695        .set(state.ids.sword_stance_agile_text, ui);
1696
1697        Text::new(
1698            &self
1699                .localized_strings
1700                .get_msg("hud-skill-sword_stance_agile"),
1701        )
1702        .x_y_position_relative_to(
1703            state.ids.sword_stance_agile_text,
1704            Relative::Scalar(2.5),
1705            Relative::Scalar(-2.5),
1706        )
1707        .depth(1.0)
1708        .font_id(self.fonts.cyri.conrod_id)
1709        .font_size(self.fonts.cyri.scale(34))
1710        .color(color::BLACK)
1711        .set(state.ids.sword_stance_agile_shadow, ui);
1712
1713        // Crippling
1714        Text::new(
1715            &self
1716                .localized_strings
1717                .get_msg("hud-skill-sword_stance_crippling"),
1718        )
1719        .mid_top_with_margin_on(state.ids.sword_stance_right_align, -7.0)
1720        .font_id(self.fonts.cyri.conrod_id)
1721        .font_size(self.fonts.cyri.scale(34))
1722        .color(Color::Rgba(0.0, 0.52, 0.0, 1.0))
1723        .set(state.ids.sword_stance_crippling_text, ui);
1724
1725        Text::new(
1726            &self
1727                .localized_strings
1728                .get_msg("hud-skill-sword_stance_crippling"),
1729        )
1730        .x_y_position_relative_to(
1731            state.ids.sword_stance_crippling_text,
1732            Relative::Scalar(2.5),
1733            Relative::Scalar(-2.5),
1734        )
1735        .depth(1.0)
1736        .font_id(self.fonts.cyri.conrod_id)
1737        .font_size(self.fonts.cyri.scale(34))
1738        .color(color::BLACK)
1739        .set(state.ids.sword_stance_crippling_shadow, ui);
1740
1741        // Heavy
1742        Text::new(
1743            &self
1744                .localized_strings
1745                .get_msg("hud-skill-sword_stance_heavy"),
1746        )
1747        .mid_bottom_with_margin_on(state.ids.sword_stance_left_align, 272.0)
1748        .font_id(self.fonts.cyri.conrod_id)
1749        .font_size(self.fonts.cyri.scale(34))
1750        .color(Color::Rgba(0.67, 0.0, 0.0, 1.0))
1751        .set(state.ids.sword_stance_heavy_text, ui);
1752
1753        Text::new(
1754            &self
1755                .localized_strings
1756                .get_msg("hud-skill-sword_stance_heavy"),
1757        )
1758        .x_y_position_relative_to(
1759            state.ids.sword_stance_heavy_text,
1760            Relative::Scalar(2.5),
1761            Relative::Scalar(-2.5),
1762        )
1763        .depth(1.0)
1764        .font_id(self.fonts.cyri.conrod_id)
1765        .font_size(self.fonts.cyri.scale(34))
1766        .color(color::BLACK)
1767        .set(state.ids.sword_stance_heavy_shadow, ui);
1768
1769        // Defensive
1770        Text::new(
1771            &self
1772                .localized_strings
1773                .get_msg("hud-skill-sword_stance_defensive"),
1774        )
1775        .mid_bottom_with_margin_on(state.ids.sword_stance_right_align, 272.0)
1776        .font_id(self.fonts.cyri.conrod_id)
1777        .font_size(self.fonts.cyri.scale(34))
1778        .color(Color::Rgba(0.10, 0.40, 0.82, 1.0))
1779        .set(state.ids.sword_stance_defensive_text, ui);
1780
1781        Text::new(
1782            &self
1783                .localized_strings
1784                .get_msg("hud-skill-sword_stance_defensive"),
1785        )
1786        .x_y_position_relative_to(
1787            state.ids.sword_stance_defensive_text,
1788            Relative::Scalar(2.5),
1789            Relative::Scalar(-2.5),
1790        )
1791        .depth(1.0)
1792        .font_id(self.fonts.cyri.conrod_id)
1793        .font_size(self.fonts.cyri.scale(34))
1794        .color(color::BLACK)
1795        .set(state.ids.sword_stance_defensive_shadow, ui);
1796
1797        use PositionSpecifier::TopLeftWithMarginsOn;
1798        let skill_buttons = &[
1799            SkillIcon::Ability {
1800                skill: Skill::Sword(SwordSkill::CrescentSlash),
1801                ability_id: "veloren.core.pseudo_abilities.sword.crescent_slash",
1802                position: TopLeftWithMarginsOn(state.ids.sword_bg, 537.0, 429.0),
1803            },
1804            SkillIcon::Ability {
1805                skill: Skill::Sword(SwordSkill::FellStrike),
1806                ability_id: "veloren.core.pseudo_abilities.sword.fell_strike",
1807                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 527.0),
1808            },
1809            SkillIcon::Ability {
1810                skill: Skill::Sword(SwordSkill::Skewer),
1811                ability_id: "veloren.core.pseudo_abilities.sword.skewer",
1812                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 527.0),
1813            },
1814            SkillIcon::Ability {
1815                skill: Skill::Sword(SwordSkill::Cascade),
1816                ability_id: "veloren.core.pseudo_abilities.sword.cascade",
1817                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 332.0),
1818            },
1819            SkillIcon::Ability {
1820                skill: Skill::Sword(SwordSkill::CrossCut),
1821                ability_id: "veloren.core.pseudo_abilities.sword.cross_cut",
1822                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 332.0),
1823            },
1824            SkillIcon::Ability {
1825                skill: Skill::Sword(SwordSkill::Finisher),
1826                ability_id: "veloren.core.pseudo_abilities.sword.finisher",
1827                position: TopLeftWithMarginsOn(state.ids.sword_bg, 263.0, 429.0),
1828            },
1829            SkillIcon::Ability {
1830                skill: Skill::Sword(SwordSkill::HeavySweep),
1831                ability_id: "common.abilities.sword.heavy_sweep",
1832                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 2.0),
1833            },
1834            SkillIcon::Ability {
1835                skill: Skill::Sword(SwordSkill::HeavyPommelStrike),
1836                ability_id: "common.abilities.sword.heavy_pommel_strike",
1837                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 91.0),
1838            },
1839            SkillIcon::Ability {
1840                skill: Skill::Sword(SwordSkill::AgileQuickDraw),
1841                ability_id: "common.abilities.sword.agile_quick_draw",
1842                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 384.0),
1843            },
1844            SkillIcon::Ability {
1845                skill: Skill::Sword(SwordSkill::AgileFeint),
1846                ability_id: "common.abilities.sword.agile_feint",
1847                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 472.0),
1848            },
1849            SkillIcon::Ability {
1850                skill: Skill::Sword(SwordSkill::DefensiveRiposte),
1851                ability_id: "common.abilities.sword.defensive_riposte",
1852                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 766.0),
1853            },
1854            SkillIcon::Ability {
1855                skill: Skill::Sword(SwordSkill::DefensiveDisengage),
1856                ability_id: "common.abilities.sword.defensive_disengage",
1857                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 855.0),
1858            },
1859            SkillIcon::Ability {
1860                skill: Skill::Sword(SwordSkill::CripplingGouge),
1861                ability_id: "common.abilities.sword.crippling_gouge",
1862                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 766.0),
1863            },
1864            SkillIcon::Ability {
1865                skill: Skill::Sword(SwordSkill::CripplingHamstring),
1866                ability_id: "common.abilities.sword.crippling_hamstring",
1867                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 766.0),
1868            },
1869            SkillIcon::Ability {
1870                skill: Skill::Sword(SwordSkill::CleavingWhirlwindSlice),
1871                ability_id: "common.abilities.sword.cleaving_whirlwind_slice",
1872                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 91.0),
1873            },
1874            SkillIcon::Ability {
1875                skill: Skill::Sword(SwordSkill::CleavingEarthSplitter),
1876                ability_id: "common.abilities.sword.cleaving_earth_splitter",
1877                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 91.0),
1878            },
1879            SkillIcon::Ability {
1880                skill: Skill::Sword(SwordSkill::HeavyFortitude),
1881                ability_id: "common.abilities.sword.heavy_fortitude",
1882                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 2.0),
1883            },
1884            SkillIcon::Ability {
1885                skill: Skill::Sword(SwordSkill::HeavyPillarThrust),
1886                ability_id: "common.abilities.sword.heavy_pillar_thrust",
1887                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 91.0),
1888            },
1889            SkillIcon::Ability {
1890                skill: Skill::Sword(SwordSkill::AgileDancingEdge),
1891                ability_id: "common.abilities.sword.agile_dancing_edge",
1892                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 385.0),
1893            },
1894            SkillIcon::Ability {
1895                skill: Skill::Sword(SwordSkill::AgileFlurry),
1896                ability_id: "common.abilities.sword.agile_flurry",
1897                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 473.0),
1898            },
1899            SkillIcon::Ability {
1900                skill: Skill::Sword(SwordSkill::DefensiveStalwartSword),
1901                ability_id: "common.abilities.sword.defensive_stalwart_sword",
1902                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 766.0),
1903            },
1904            SkillIcon::Ability {
1905                skill: Skill::Sword(SwordSkill::DefensiveDeflect),
1906                ability_id: "common.abilities.sword.defensive_deflect",
1907                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 855.0),
1908            },
1909            SkillIcon::Ability {
1910                skill: Skill::Sword(SwordSkill::CripplingEviscerate),
1911                ability_id: "common.abilities.sword.crippling_eviscerate",
1912                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 855.0),
1913            },
1914            SkillIcon::Ability {
1915                skill: Skill::Sword(SwordSkill::CripplingBloodyGash),
1916                ability_id: "common.abilities.sword.crippling_bloody_gash",
1917                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 855.0),
1918            },
1919            SkillIcon::Ability {
1920                skill: Skill::Sword(SwordSkill::CleavingBladeFever),
1921                ability_id: "common.abilities.sword.cleaving_blade_fever",
1922                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 2.0),
1923            },
1924            SkillIcon::Ability {
1925                skill: Skill::Sword(SwordSkill::CleavingSkySplitter),
1926                ability_id: "common.abilities.sword.cleaving_sky_splitter",
1927                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 2.0),
1928            },
1929        ];
1930
1931        state.update(|s| {
1932            s.ids
1933                .skills
1934                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
1935        });
1936        state.update(|s| {
1937            s.ids
1938                .skill_lock_imgs
1939                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
1940        });
1941
1942        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
1943        events
1944    }
1945
1946    fn handle_axe_skills_window(
1947        &mut self,
1948        diary_tooltip: &Tooltip,
1949        state: &mut State<DiaryState>,
1950        ui: &mut UiCell,
1951        mut events: Vec<Event>,
1952    ) -> Vec<Event> {
1953        // Axe
1954        Image::new(self.imgs.axe_bg)
1955            .wh([924.0, 619.0])
1956            .mid_top_with_margin_on(state.ids.content_align, 65.0)
1957            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1958            .set(state.ids.axe_bg, ui);
1959
1960        use PositionSpecifier::TopLeftWithMarginsOn;
1961        let skill_buttons = &[
1962            SkillIcon::Ability {
1963                skill: Skill::Axe(AxeSkill::BrutalSwing),
1964                ability_id: "common.abilities.axe.brutal_swing",
1965                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 424.0),
1966            },
1967            SkillIcon::Ability {
1968                skill: Skill::Axe(AxeSkill::Berserk),
1969                ability_id: "common.abilities.axe.berserk",
1970                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 374.0),
1971            },
1972            SkillIcon::Ability {
1973                skill: Skill::Axe(AxeSkill::RisingTide),
1974                ability_id: "common.abilities.axe.rising_tide",
1975                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 474.0),
1976            },
1977            SkillIcon::Ability {
1978                skill: Skill::Axe(AxeSkill::SavageSense),
1979                ability_id: "common.abilities.axe.savage_sense",
1980                position: TopLeftWithMarginsOn(state.ids.axe_bg, 187.0, 324.0),
1981            },
1982            SkillIcon::Ability {
1983                skill: Skill::Axe(AxeSkill::AdrenalineRush),
1984                ability_id: "common.abilities.axe.adrenaline_rush",
1985                position: TopLeftWithMarginsOn(state.ids.axe_bg, 187.0, 524.0),
1986            },
1987            SkillIcon::Ability {
1988                skill: Skill::Axe(AxeSkill::Execute),
1989                ability_id: "common.abilities.axe.execute",
1990                position: TopLeftWithMarginsOn(state.ids.axe_bg, 187.0, 424.0),
1991            },
1992            SkillIcon::Ability {
1993                skill: Skill::Axe(AxeSkill::Maelstrom),
1994                ability_id: "common.abilities.axe.maelstrom",
1995                position: TopLeftWithMarginsOn(state.ids.axe_bg, 4.0, 424.0),
1996            },
1997            SkillIcon::Ability {
1998                skill: Skill::Axe(AxeSkill::Rake),
1999                ability_id: "common.abilities.axe.rake",
2000                position: TopLeftWithMarginsOn(state.ids.axe_bg, 507.0, 325.0),
2001            },
2002            SkillIcon::Ability {
2003                skill: Skill::Axe(AxeSkill::Bloodfeast),
2004                ability_id: "common.abilities.axe.bloodfeast",
2005                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 74.0),
2006            },
2007            SkillIcon::Ability {
2008                skill: Skill::Axe(AxeSkill::FierceRaze),
2009                ability_id: "common.abilities.axe.fierce_raze",
2010                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 174.0),
2011            },
2012            SkillIcon::Ability {
2013                skill: Skill::Axe(AxeSkill::Furor),
2014                ability_id: "common.abilities.axe.furor",
2015                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 24.0),
2016            },
2017            SkillIcon::Ability {
2018                skill: Skill::Axe(AxeSkill::Fracture),
2019                ability_id: "common.abilities.axe.fracture",
2020                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 224.0),
2021            },
2022            SkillIcon::Ability {
2023                skill: Skill::Axe(AxeSkill::Lacerate),
2024                ability_id: "common.abilities.axe.lacerate",
2025                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 124.0),
2026            },
2027            SkillIcon::Ability {
2028                skill: Skill::Axe(AxeSkill::Riptide),
2029                ability_id: "common.abilities.axe.riptide",
2030                position: TopLeftWithMarginsOn(state.ids.axe_bg, 104.0, 124.0),
2031            },
2032            SkillIcon::Ability {
2033                skill: Skill::Axe(AxeSkill::SkullBash),
2034                ability_id: "common.abilities.axe.skull_bash",
2035                position: TopLeftWithMarginsOn(state.ids.axe_bg, 507.0, 523.0),
2036            },
2037            SkillIcon::Ability {
2038                skill: Skill::Axe(AxeSkill::Sunder),
2039                ability_id: "common.abilities.axe.sunder",
2040                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 674.0),
2041            },
2042            SkillIcon::Ability {
2043                skill: Skill::Axe(AxeSkill::Plunder),
2044                ability_id: "common.abilities.axe.plunder",
2045                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 774.0),
2046            },
2047            SkillIcon::Ability {
2048                skill: Skill::Axe(AxeSkill::Defiance),
2049                ability_id: "common.abilities.axe.defiance",
2050                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 624.0),
2051            },
2052            SkillIcon::Ability {
2053                skill: Skill::Axe(AxeSkill::Keelhaul),
2054                ability_id: "common.abilities.axe.keelhaul",
2055                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 824.0),
2056            },
2057            SkillIcon::Ability {
2058                skill: Skill::Axe(AxeSkill::Bulkhead),
2059                ability_id: "common.abilities.axe.bulkhead",
2060                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 724.0),
2061            },
2062            SkillIcon::Ability {
2063                skill: Skill::Axe(AxeSkill::Capsize),
2064                ability_id: "common.abilities.axe.capsize",
2065                position: TopLeftWithMarginsOn(state.ids.axe_bg, 104.0, 724.0),
2066            },
2067        ];
2068
2069        state.update(|s| {
2070            s.ids
2071                .skills
2072                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2073        });
2074        state.update(|s| {
2075            s.ids
2076                .skill_lock_imgs
2077                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2078        });
2079
2080        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2081        events
2082    }
2083
2084    fn handle_hammer_skills_window(
2085        &mut self,
2086        diary_tooltip: &Tooltip,
2087        state: &mut State<DiaryState>,
2088        ui: &mut UiCell,
2089        mut events: Vec<Event>,
2090    ) -> Vec<Event> {
2091        // Hammer
2092        Image::new(self.imgs.hammer_bg)
2093            .wh([924.0, 619.0])
2094            .mid_top_with_margin_on(state.ids.content_align, 65.0)
2095            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2096            .set(state.ids.hammer_bg, ui);
2097
2098        use PositionSpecifier::TopLeftWithMarginsOn;
2099        let skill_buttons = &[
2100            SkillIcon::Ability {
2101                skill: Skill::Hammer(HammerSkill::ScornfulSwipe),
2102                ability_id: "common.abilities.hammer.scornful_swipe",
2103                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 455.0, 424.0),
2104            },
2105            SkillIcon::Ability {
2106                skill: Skill::Hammer(HammerSkill::Tremor),
2107                ability_id: "common.abilities.hammer.tremor",
2108                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 172.0),
2109            },
2110            SkillIcon::Ability {
2111                skill: Skill::Hammer(HammerSkill::VigorousBash),
2112                ability_id: "common.abilities.hammer.vigorous_bash",
2113                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 272.0),
2114            },
2115            SkillIcon::Ability {
2116                skill: Skill::Hammer(HammerSkill::Retaliate),
2117                ability_id: "common.abilities.hammer.retaliate",
2118                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 122.0),
2119            },
2120            SkillIcon::Ability {
2121                skill: Skill::Hammer(HammerSkill::SpineCracker),
2122                ability_id: "common.abilities.hammer.spine_cracker",
2123                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 222.0),
2124            },
2125            SkillIcon::Ability {
2126                skill: Skill::Hammer(HammerSkill::Breach),
2127                ability_id: "common.abilities.hammer.breach",
2128                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 322.0),
2129            },
2130            SkillIcon::Ability {
2131                skill: Skill::Hammer(HammerSkill::IronTempest),
2132                ability_id: "common.abilities.hammer.iron_tempest",
2133                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 172.0),
2134            },
2135            SkillIcon::Ability {
2136                skill: Skill::Hammer(HammerSkill::Upheaval),
2137                ability_id: "common.abilities.hammer.upheaval",
2138                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 272.0),
2139            },
2140            SkillIcon::Ability {
2141                skill: Skill::Hammer(HammerSkill::Thunderclap),
2142                ability_id: "common.abilities.hammer.thunderclap",
2143                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 172.0),
2144            },
2145            SkillIcon::Ability {
2146                skill: Skill::Hammer(HammerSkill::SeismicShock),
2147                ability_id: "common.abilities.hammer.seismic_shock",
2148                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 272.0),
2149            },
2150            SkillIcon::Ability {
2151                skill: Skill::Hammer(HammerSkill::HeavyWhorl),
2152                ability_id: "common.abilities.hammer.heavy_whorl",
2153                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 576.0),
2154            },
2155            SkillIcon::Ability {
2156                skill: Skill::Hammer(HammerSkill::Intercept),
2157                ability_id: "common.abilities.hammer.intercept",
2158                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 676.0),
2159            },
2160            SkillIcon::Ability {
2161                skill: Skill::Hammer(HammerSkill::PileDriver),
2162                ability_id: "common.abilities.hammer.pile_driver",
2163                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 526.0),
2164            },
2165            SkillIcon::Ability {
2166                skill: Skill::Hammer(HammerSkill::LungPummel),
2167                ability_id: "common.abilities.hammer.lung_pummel",
2168                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 626.0),
2169            },
2170            SkillIcon::Ability {
2171                skill: Skill::Hammer(HammerSkill::HelmCrusher),
2172                ability_id: "common.abilities.hammer.helm_crusher",
2173                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 726.0),
2174            },
2175            SkillIcon::Ability {
2176                skill: Skill::Hammer(HammerSkill::Rampart),
2177                ability_id: "common.abilities.hammer.rampart",
2178                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 576.0),
2179            },
2180            SkillIcon::Ability {
2181                skill: Skill::Hammer(HammerSkill::Tenacity),
2182                ability_id: "common.abilities.hammer.tenacity",
2183                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 676.0),
2184            },
2185            SkillIcon::Ability {
2186                skill: Skill::Hammer(HammerSkill::Earthshaker),
2187                ability_id: "common.abilities.hammer.earthshaker",
2188                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 576.0),
2189            },
2190            SkillIcon::Ability {
2191                skill: Skill::Hammer(HammerSkill::Judgement),
2192                ability_id: "common.abilities.hammer.judgement",
2193                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 676.0),
2194            },
2195        ];
2196
2197        state.update(|s| {
2198            s.ids
2199                .skills
2200                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2201        });
2202        state.update(|s| {
2203            s.ids
2204                .skill_lock_imgs
2205                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2206        });
2207
2208        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2209        events
2210    }
2211
2212    fn handle_sceptre_skills_window(
2213        &mut self,
2214        diary_tooltip: &Tooltip,
2215        state: &mut State<DiaryState>,
2216        ui: &mut UiCell,
2217        mut events: Vec<Event>,
2218    ) -> Vec<Event> {
2219        // Title text
2220        let tree_title = &self.localized_strings.get_msg("common-weapons-sceptre");
2221
2222        Text::new(tree_title)
2223            .mid_top_with_margin_on(state.ids.content_align, 2.0)
2224            .font_id(self.fonts.cyri.conrod_id)
2225            .font_size(self.fonts.cyri.scale(34))
2226            .color(TEXT_COLOR)
2227            .set(state.ids.tree_title_txt, ui);
2228
2229        // Number of skills per rectangle per weapon, start counting at 0
2230        // Maximum of 9 skills/8 indices
2231        let skills_top_l = 5;
2232        let skills_top_r = 5;
2233        let skills_bot_l = 5;
2234        let skills_bot_r = 0;
2235
2236        self.setup_state_for_skill_icons(
2237            state,
2238            ui,
2239            skills_top_l,
2240            skills_top_r,
2241            skills_bot_l,
2242            skills_bot_r,
2243        );
2244
2245        // Skill icons and buttons
2246        use skills::SceptreSkill::*;
2247        // Sceptre
2248        Image::new(animate_by_pulse(
2249            &self
2250                .item_imgs
2251                .img_ids_or_not_found_img(ItemKey::Simple("example_sceptre".to_string())),
2252            self.pulse,
2253        ))
2254        .wh(ART_SIZE)
2255        .middle_of(state.ids.content_align)
2256        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2257        .set(state.ids.sceptre_render, ui);
2258        use PositionSpecifier::MidTopWithMarginOn;
2259        let skill_buttons = &[
2260            // Top Left skills
2261            //        5 1 6
2262            //        3 0 4
2263            //        8 2 7
2264            SkillIcon::Descriptive {
2265                title: "hud-skill-sc_lifesteal_title",
2266                desc: "hud-skill-sc_lifesteal",
2267                image: self.imgs.skill_sceptre_lifesteal,
2268                position: MidTopWithMarginOn(state.ids.skills_top_l[0], 3.0),
2269                id: state.ids.skill_sceptre_lifesteal_0,
2270            },
2271            SkillIcon::Unlockable {
2272                skill: Skill::Sceptre(LDamage),
2273                image: self.imgs.magic_damage_skill,
2274                position: MidTopWithMarginOn(state.ids.skills_top_l[1], 3.0),
2275                id: state.ids.skill_sceptre_lifesteal_1,
2276            },
2277            SkillIcon::Unlockable {
2278                skill: Skill::Sceptre(LRange),
2279                image: self.imgs.magic_distance_skill,
2280                position: MidTopWithMarginOn(state.ids.skills_top_l[2], 3.0),
2281                id: state.ids.skill_sceptre_lifesteal_2,
2282            },
2283            SkillIcon::Unlockable {
2284                skill: Skill::Sceptre(LLifesteal),
2285                image: self.imgs.magic_lifesteal_skill,
2286                position: MidTopWithMarginOn(state.ids.skills_top_l[3], 3.0),
2287                id: state.ids.skill_sceptre_lifesteal_3,
2288            },
2289            SkillIcon::Unlockable {
2290                skill: Skill::Sceptre(LRegen),
2291                image: self.imgs.magic_energy_regen_skill,
2292                position: MidTopWithMarginOn(state.ids.skills_top_l[4], 3.0),
2293                id: state.ids.skill_sceptre_lifesteal_4,
2294            },
2295            // Top right skills
2296            SkillIcon::Descriptive {
2297                title: "hud-skill-sc_heal_title",
2298                desc: "hud-skill-sc_heal",
2299                image: self.imgs.skill_sceptre_heal,
2300                position: MidTopWithMarginOn(state.ids.skills_top_r[0], 3.0),
2301                id: state.ids.skill_sceptre_heal_0,
2302            },
2303            SkillIcon::Unlockable {
2304                skill: Skill::Sceptre(HHeal),
2305                image: self.imgs.heal_heal_skill,
2306                position: MidTopWithMarginOn(state.ids.skills_top_r[1], 3.0),
2307                id: state.ids.skill_sceptre_heal_1,
2308            },
2309            SkillIcon::Unlockable {
2310                skill: Skill::Sceptre(HDuration),
2311                image: self.imgs.heal_duration_skill,
2312                position: MidTopWithMarginOn(state.ids.skills_top_r[2], 3.0),
2313                id: state.ids.skill_sceptre_heal_2,
2314            },
2315            SkillIcon::Unlockable {
2316                skill: Skill::Sceptre(HRange),
2317                image: self.imgs.heal_radius_skill,
2318                position: MidTopWithMarginOn(state.ids.skills_top_r[3], 3.0),
2319                id: state.ids.skill_sceptre_heal_3,
2320            },
2321            SkillIcon::Unlockable {
2322                skill: Skill::Sceptre(HCost),
2323                image: self.imgs.heal_cost_skill,
2324                position: MidTopWithMarginOn(state.ids.skills_top_r[4], 3.0),
2325                id: state.ids.skill_sceptre_heal_4,
2326            },
2327            // Bottom left skills
2328            SkillIcon::Unlockable {
2329                skill: Skill::Sceptre(UnlockAura),
2330                image: self.imgs.skill_sceptre_aura,
2331                position: MidTopWithMarginOn(state.ids.skills_bot_l[0], 3.0),
2332                id: state.ids.skill_sceptre_aura_0,
2333            },
2334            SkillIcon::Unlockable {
2335                skill: Skill::Sceptre(AStrength),
2336                image: self.imgs.buff_damage_skill,
2337                position: MidTopWithMarginOn(state.ids.skills_bot_l[1], 3.0),
2338                id: state.ids.skill_sceptre_aura_1,
2339            },
2340            SkillIcon::Unlockable {
2341                skill: Skill::Sceptre(ADuration),
2342                image: self.imgs.buff_duration_skill,
2343                position: MidTopWithMarginOn(state.ids.skills_bot_l[2], 3.0),
2344                id: state.ids.skill_sceptre_aura_2,
2345            },
2346            SkillIcon::Unlockable {
2347                skill: Skill::Sceptre(ARange),
2348                image: self.imgs.buff_radius_skill,
2349                position: MidTopWithMarginOn(state.ids.skills_bot_l[3], 3.0),
2350                id: state.ids.skill_sceptre_aura_3,
2351            },
2352            SkillIcon::Unlockable {
2353                skill: Skill::Sceptre(ACost),
2354                image: self.imgs.buff_cost_skill,
2355                position: MidTopWithMarginOn(state.ids.skills_bot_l[4], 3.0),
2356                id: state.ids.skill_sceptre_aura_4,
2357            },
2358        ];
2359
2360        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2361        events
2362    }
2363
2364    fn handle_bow_skills_window(
2365        &mut self,
2366        diary_tooltip: &Tooltip,
2367        state: &mut State<DiaryState>,
2368        ui: &mut UiCell,
2369        mut events: Vec<Event>,
2370    ) -> Vec<Event> {
2371        Image::new(self.imgs.bow_bg)
2372            .wh([924.0, 619.0])
2373            .mid_top_with_margin_on(state.ids.content_align, 65.0)
2374            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2375            .set(state.ids.bow_bg, ui);
2376
2377        use PositionSpecifier::TopLeftWithMarginsOn;
2378        let skill_buttons = &[
2379            SkillIcon::Ability {
2380                skill: Skill::Bow(BowSkill::Foothold),
2381                ability_id: "common.abilities.bow.foothold",
2382                position: TopLeftWithMarginsOn(state.ids.bow_bg, 424.0, 368.0),
2383            },
2384            SkillIcon::Ability {
2385                skill: Skill::Bow(BowSkill::HeavyNock),
2386                ability_id: "common.abilities.bow.heavy_nock",
2387                position: TopLeftWithMarginsOn(state.ids.bow_bg, 424.0, 480.0),
2388            },
2389            SkillIcon::Ability {
2390                skill: Skill::Bow(BowSkill::ArdentHunt),
2391                ability_id: "common.abilities.bow.ardent_hunt",
2392                position: TopLeftWithMarginsOn(state.ids.bow_bg, 310.0, 204.0),
2393            },
2394            SkillIcon::Ability {
2395                skill: Skill::Bow(BowSkill::SepticShot),
2396                ability_id: "common.abilities.bow.septic_shot",
2397                position: TopLeftWithMarginsOn(state.ids.bow_bg, 310.0, 424.0),
2398            },
2399            SkillIcon::Ability {
2400                skill: Skill::Bow(BowSkill::Barrage),
2401                ability_id: "common.abilities.bow.barrage",
2402                position: TopLeftWithMarginsOn(state.ids.bow_bg, 310.0, 644.0),
2403            },
2404            SkillIcon::Ability {
2405                skill: Skill::Bow(BowSkill::StormChaser),
2406                ability_id: "common.abilities.bow.storm_chaser",
2407                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 154.0),
2408            },
2409            SkillIcon::Ability {
2410                skill: Skill::Bow(BowSkill::EagleEye),
2411                ability_id: "common.abilities.bow.eagle_eye",
2412                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 254.0),
2413            },
2414            SkillIcon::Ability {
2415                skill: Skill::Bow(BowSkill::IgniteArrow),
2416                ability_id: "common.abilities.bow.ignite_arrow",
2417                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 374.0),
2418            },
2419            SkillIcon::Ability {
2420                skill: Skill::Bow(BowSkill::DrenchArrow),
2421                ability_id: "common.abilities.bow.drench_arrow",
2422                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 474.0),
2423            },
2424            SkillIcon::Ability {
2425                skill: Skill::Bow(BowSkill::PiercingGale),
2426                ability_id: "common.abilities.bow.piercing_gale",
2427                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 594.0),
2428            },
2429            SkillIcon::Ability {
2430                skill: Skill::Bow(BowSkill::ThornStake),
2431                ability_id: "common.abilities.bow.thorn_stake",
2432                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 694.0),
2433            },
2434            SkillIcon::Ability {
2435                skill: Skill::Bow(BowSkill::Heartseeker),
2436                ability_id: "common.abilities.bow.heartseeker",
2437                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 154.0),
2438            },
2439            SkillIcon::Ability {
2440                skill: Skill::Bow(BowSkill::Hawkstrike),
2441                ability_id: "common.abilities.bow.hawkstrike",
2442                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 254.0),
2443            },
2444            SkillIcon::Ability {
2445                skill: Skill::Bow(BowSkill::FreezeArrow),
2446                ability_id: "common.abilities.bow.freeze_arrow",
2447                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 374.0),
2448            },
2449            SkillIcon::Ability {
2450                skill: Skill::Bow(BowSkill::JoltArrow),
2451                ability_id: "common.abilities.bow.jolt_arrow",
2452                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 474.0),
2453            },
2454            SkillIcon::Ability {
2455                skill: Skill::Bow(BowSkill::Fusillade),
2456                ability_id: "common.abilities.bow.fusillade",
2457                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 594.0),
2458            },
2459            SkillIcon::Ability {
2460                skill: Skill::Bow(BowSkill::DeathVolley),
2461                ability_id: "common.abilities.bow.death_volley",
2462                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 694.0),
2463            },
2464        ];
2465
2466        state.update(|s| {
2467            s.ids
2468                .skills
2469                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2470        });
2471        state.update(|s| {
2472            s.ids
2473                .skill_lock_imgs
2474                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2475        });
2476
2477        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2478        events
2479    }
2480
2481    fn handle_staff_skills_window(
2482        &mut self,
2483        diary_tooltip: &Tooltip,
2484        state: &mut State<DiaryState>,
2485        ui: &mut UiCell,
2486        mut events: Vec<Event>,
2487    ) -> Vec<Event> {
2488        use skills::StaffSkill::*;
2489
2490        Image::new(self.imgs.staff_bg)
2491            .wh([924.0, 619.0])
2492            .mid_top_with_margin_on(state.ids.content_align, 65.0)
2493            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2494            .set(state.ids.staff_bg, ui);
2495
2496        use PositionSpecifier::TopLeftWithMarginsOn;
2497        let skill_buttons = &[
2498            SkillIcon::Ability {
2499                skill: Skill::Staff(FireShockwave),
2500                ability_id: "common.abilities.staff.fireshockwave",
2501                position: TopLeftWithMarginsOn(state.ids.staff_bg, 436.0, 421.0),
2502            },
2503            SkillIcon::Ability {
2504                skill: Skill::Staff(FireDash),
2505                ability_id: "common.abilities.staff.fire_dash",
2506                position: TopLeftWithMarginsOn(state.ids.staff_bg, 319.0, 331.0),
2507            },
2508            SkillIcon::Ability {
2509                skill: Skill::Staff(FlameCloak),
2510                ability_id: "common.abilities.staff.flame_cloak",
2511                position: TopLeftWithMarginsOn(state.ids.staff_bg, 319.0, 515.0),
2512            },
2513            SkillIcon::Ability {
2514                skill: Skill::Staff(FireBreath),
2515                ability_id: "common.abilities.staff.fire_breath",
2516                position: TopLeftWithMarginsOn(state.ids.staff_bg, 200.0, 515.0),
2517            },
2518            SkillIcon::Ability {
2519                skill: Skill::Staff(NapalmStrike),
2520                ability_id: "common.abilities.staff.napalm_strike",
2521                position: TopLeftWithMarginsOn(state.ids.staff_bg, 200.0, 331.0),
2522            },
2523            SkillIcon::Ability {
2524                skill: Skill::Staff(Pyroclasm),
2525                ability_id: "common.abilities.staff.pyroclasm",
2526                position: TopLeftWithMarginsOn(state.ids.staff_bg, 86.0, 422.0),
2527            },
2528        ];
2529
2530        state.update(|s| {
2531            s.ids
2532                .skills
2533                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2534        });
2535        state.update(|s| {
2536            s.ids
2537                .skill_lock_imgs
2538                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2539        });
2540
2541        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2542        events
2543    }
2544
2545    fn handle_mining_skills_window(
2546        &mut self,
2547        diary_tooltip: &Tooltip,
2548        state: &mut State<DiaryState>,
2549        ui: &mut UiCell,
2550        mut events: Vec<Event>,
2551    ) -> Vec<Event> {
2552        // Title text
2553        let tree_title = &self.localized_strings.get_msg("common-tool-mining");
2554
2555        Text::new(tree_title)
2556            .mid_top_with_margin_on(state.ids.content_align, 2.0)
2557            .font_id(self.fonts.cyri.conrod_id)
2558            .font_size(self.fonts.cyri.scale(34))
2559            .color(TEXT_COLOR)
2560            .set(state.ids.tree_title_txt, ui);
2561
2562        // Number of skills per rectangle per weapon, start counting at 0
2563        // Maximum of 9 skills/8 indices
2564        let skills_top_l = 4;
2565        let skills_top_r = 0;
2566        let skills_bot_l = 0;
2567        let skills_bot_r = 0;
2568
2569        self.setup_state_for_skill_icons(
2570            state,
2571            ui,
2572            skills_top_l,
2573            skills_top_r,
2574            skills_bot_l,
2575            skills_bot_r,
2576        );
2577
2578        // Skill icons and buttons
2579        use skills::MiningSkill::*;
2580        // Mining
2581        Image::new(animate_by_pulse(
2582            &self
2583                .item_imgs
2584                .img_ids_or_not_found_img(ItemKey::Simple("example_pick".to_string())),
2585            self.pulse,
2586        ))
2587        .wh(ART_SIZE)
2588        .middle_of(state.ids.content_align)
2589        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2590        .set(state.ids.pick_render, ui);
2591
2592        use PositionSpecifier::MidTopWithMarginOn;
2593        let skill_buttons = &[
2594            // Top Left skills
2595            //        5 1 6
2596            //        3 0 4
2597            //        8 2 7
2598            SkillIcon::Descriptive {
2599                title: "hud-skill-pick_strike_title",
2600                desc: "hud-skill-pick_strike",
2601                image: self.imgs.pickaxe,
2602                position: MidTopWithMarginOn(state.ids.skills_top_l[0], 3.0),
2603                id: state.ids.skill_pick_m1,
2604            },
2605            SkillIcon::Unlockable {
2606                skill: Skill::Pick(Speed),
2607                image: self.imgs.pickaxe_speed_skill,
2608                position: MidTopWithMarginOn(state.ids.skills_top_l[1], 3.0),
2609                id: state.ids.skill_pick_m1_0,
2610            },
2611            SkillIcon::Unlockable {
2612                skill: Skill::Pick(OreGain),
2613                image: self.imgs.pickaxe_oregain_skill,
2614                position: MidTopWithMarginOn(state.ids.skills_top_l[2], 3.0),
2615                id: state.ids.skill_pick_m1_1,
2616            },
2617            SkillIcon::Unlockable {
2618                skill: Skill::Pick(GemGain),
2619                image: self.imgs.pickaxe_gemgain_skill,
2620                position: MidTopWithMarginOn(state.ids.skills_top_l[3], 3.0),
2621                id: state.ids.skill_pick_m1_2,
2622            },
2623        ];
2624
2625        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2626        events
2627    }
2628
2629    fn handle_skill_buttons(
2630        &mut self,
2631        icons: &[SkillIcon],
2632        ui: &mut UiCell,
2633        events: &mut Vec<Event>,
2634        diary_tooltip: &Tooltip,
2635        state: &mut State<DiaryState>,
2636    ) {
2637        for (i, icon) in icons.iter().enumerate() {
2638            match icon {
2639                SkillIcon::Descriptive {
2640                    title,
2641                    desc,
2642                    image,
2643                    position,
2644                    id,
2645                } => {
2646                    // TODO: shouldn't this be a `Image::new`?
2647                    Button::image(*image)
2648                        .w_h(74.0, 74.0)
2649                        .position(*position)
2650                        .with_tooltip(
2651                            self.tooltip_manager,
2652                            &self.localized_strings.get_msg(title),
2653                            &self.localized_strings.get_msg(desc),
2654                            diary_tooltip,
2655                            TEXT_COLOR,
2656                        )
2657                        .set(*id, ui);
2658                },
2659                SkillIcon::Unlockable {
2660                    skill,
2661                    image,
2662                    position,
2663                    id,
2664                } => self.create_unlock_skill_button(
2665                    *skill,
2666                    *image,
2667                    *position,
2668                    *id,
2669                    ui,
2670                    events,
2671                    diary_tooltip,
2672                ),
2673                SkillIcon::Ability {
2674                    skill,
2675                    ability_id,
2676                    position,
2677                } => self.create_unlock_ability_button(
2678                    *skill,
2679                    ability_id,
2680                    *position,
2681                    i,
2682                    ui,
2683                    events,
2684                    diary_tooltip,
2685                    state,
2686                ),
2687            }
2688        }
2689    }
2690
2691    fn setup_state_for_skill_icons(
2692        &mut self,
2693        state: &mut State<DiaryState>,
2694        ui: &mut UiCell,
2695        skills_top_l: usize,
2696        skills_top_r: usize,
2697        skills_bot_l: usize,
2698        skills_bot_r: usize,
2699    ) {
2700        // Update widget id array len
2701        state.update(|s| {
2702            s.ids
2703                .skills_top_l
2704                .resize(skills_top_l, &mut ui.widget_id_generator())
2705        });
2706        state.update(|s| {
2707            s.ids
2708                .skills_top_r
2709                .resize(skills_top_r, &mut ui.widget_id_generator())
2710        });
2711        state.update(|s| {
2712            s.ids
2713                .skills_bot_l
2714                .resize(skills_bot_l, &mut ui.widget_id_generator())
2715        });
2716        state.update(|s| {
2717            s.ids
2718                .skills_bot_r
2719                .resize(skills_bot_r, &mut ui.widget_id_generator())
2720        });
2721
2722        // Create Background Images to place skill icons on them later
2723        // Create central skill first, others around it:
2724        //
2725        //        5 1 6
2726        //        3 0 4
2727        //        8 2 7
2728        //
2729        //
2730        let offset_0 = 22.0;
2731        let offset_1 = -122.0;
2732        let offset_2 = offset_1 - -20.0;
2733
2734        let skill_pos = |idx, align, central_skill| {
2735            use PositionSpecifier::*;
2736            match idx {
2737                // Central skill
2738                0 => MiddleOf(align),
2739                // 12:00
2740                1 => UpFrom(central_skill, offset_0),
2741                // 6:00
2742                2 => DownFrom(central_skill, offset_0),
2743                // 3:00
2744                3 => LeftFrom(central_skill, offset_0),
2745                // 9:00
2746                4 => RightFrom(central_skill, offset_0),
2747                // 10:30
2748                5 => TopLeftWithMarginsOn(central_skill, offset_1, offset_2),
2749                // 1:30
2750                6 => TopRightWithMarginsOn(central_skill, offset_1, offset_2),
2751                // 4:30
2752                7 => BottomLeftWithMarginsOn(central_skill, offset_1, offset_2),
2753                // 7:30
2754                8 => BottomRightWithMarginsOn(central_skill, offset_1, offset_2),
2755                buttons => {
2756                    panic!("{} > 8 position number", buttons);
2757                },
2758            }
2759        };
2760
2761        // TOP-LEFT Skills
2762        //
2763        // TODO: Why this uses while loop on field of struct and not just
2764        // `for i in 0..skils_top_l`?
2765        while self.created_btns_top_l < skills_top_l {
2766            let pos = skill_pos(
2767                self.created_btns_top_l,
2768                state.ids.skills_top_l_align,
2769                state.ids.skills_top_l[0],
2770            );
2771            Button::image(self.imgs.wpn_icon_border_skills)
2772                .w_h(80.0, 100.0)
2773                .position(pos)
2774                .set(state.ids.skills_top_l[self.created_btns_top_l], ui);
2775            self.created_btns_top_l += 1;
2776        }
2777        // TOP-RIGHT Skills
2778        while self.created_btns_top_r < skills_top_r {
2779            let pos = skill_pos(
2780                self.created_btns_top_r,
2781                state.ids.skills_top_r_align,
2782                state.ids.skills_top_r[0],
2783            );
2784            Button::image(self.imgs.wpn_icon_border_skills)
2785                .w_h(80.0, 100.0)
2786                .position(pos)
2787                .set(state.ids.skills_top_r[self.created_btns_top_r], ui);
2788            self.created_btns_top_r += 1;
2789        }
2790        // BOTTOM-LEFT Skills
2791        while self.created_btns_bot_l < skills_bot_l {
2792            let pos = skill_pos(
2793                self.created_btns_bot_l,
2794                state.ids.skills_bot_l_align,
2795                state.ids.skills_bot_l[0],
2796            );
2797            Button::image(self.imgs.wpn_icon_border_skills)
2798                .w_h(80.0, 100.0)
2799                .position(pos)
2800                .set(state.ids.skills_bot_l[self.created_btns_bot_l], ui);
2801            self.created_btns_bot_l += 1;
2802        }
2803        // BOTTOM-RIGHT Skills
2804        while self.created_btns_bot_r < skills_bot_r {
2805            let pos = skill_pos(
2806                self.created_btns_bot_r,
2807                state.ids.skills_bot_r_align,
2808                state.ids.skills_bot_r[0],
2809            );
2810            Button::image(self.imgs.wpn_icon_border_skills)
2811                .w_h(80.0, 100.0)
2812                .position(pos)
2813                .set(state.ids.skills_bot_r[self.created_btns_bot_r], ui);
2814            self.created_btns_bot_r += 1;
2815        }
2816    }
2817
2818    fn create_unlock_skill_button(
2819        &mut self,
2820        skill: Skill,
2821        skill_image: image::Id,
2822        position: PositionSpecifier,
2823        widget_id: widget::Id,
2824        ui: &mut UiCell,
2825        events: &mut Vec<Event>,
2826        diary_tooltip: &Tooltip,
2827    ) {
2828        let label = if self.skill_set.prerequisites_met(skill) {
2829            let current = self.skill_set.skill_level(skill).unwrap_or(0);
2830            let max = skill.max_level();
2831            format!("{}/{}", current, max)
2832        } else {
2833            "".to_owned()
2834        };
2835
2836        let label_color = if self.skill_set.is_at_max_level(skill) {
2837            TEXT_COLOR
2838        } else if self.skill_set.sufficient_skill_points(skill) {
2839            HP_COLOR
2840        } else {
2841            CRITICAL_HP_COLOR
2842        };
2843
2844        let image_color = if self.skill_set.prerequisites_met(skill) {
2845            TEXT_COLOR
2846        } else {
2847            Color::Rgba(0.41, 0.41, 0.41, 0.7)
2848        };
2849
2850        let skill_strings = skill_strings(skill);
2851        let (title, description) =
2852            skill_strings.localize(self.localized_strings, self.skill_set, skill);
2853
2854        let button = Button::image(skill_image)
2855            .w_h(74.0, 74.0)
2856            .position(position)
2857            .label(&label)
2858            .label_y(conrod_core::position::Relative::Scalar(-47.0))
2859            .label_x(conrod_core::position::Relative::Scalar(0.0))
2860            .label_color(label_color)
2861            .label_font_size(self.fonts.cyri.scale(15))
2862            .label_font_id(self.fonts.cyri.conrod_id)
2863            .image_color(image_color)
2864            .with_tooltip(
2865                self.tooltip_manager,
2866                &title,
2867                &description,
2868                diary_tooltip,
2869                TEXT_COLOR,
2870            )
2871            .set(widget_id, ui);
2872
2873        if button.was_clicked() {
2874            events.push(Event::UnlockSkill(skill));
2875        };
2876    }
2877
2878    fn create_unlock_ability_button(
2879        &mut self,
2880        skill: Skill,
2881        ability_id: &str,
2882        position: PositionSpecifier,
2883        widget_index: usize,
2884        ui: &mut UiCell,
2885        events: &mut Vec<Event>,
2886        diary_tooltip: &Tooltip,
2887        state: &mut State<DiaryState>,
2888    ) {
2889        let locked = !self.skill_set.prerequisites_met(skill);
2890        let owned = self.skill_set.has_skill(skill);
2891        let image_color = if owned {
2892            TEXT_COLOR
2893        } else {
2894            Color::Rgba(0.41, 0.41, 0.41, 0.7)
2895        };
2896
2897        let (title, description) = util::ability_description(ability_id, self.localized_strings);
2898
2899        let sp_cost = sp(self.localized_strings, self.skill_set, skill);
2900
2901        let description = format!("{description}\n{sp_cost}");
2902
2903        let button = Button::image(util::ability_image(self.imgs, ability_id))
2904            .w_h(76.0, 76.0)
2905            .position(position)
2906            .image_color(image_color)
2907            .with_tooltip(
2908                self.tooltip_manager,
2909                &title,
2910                &description,
2911                diary_tooltip,
2912                TEXT_COLOR,
2913            )
2914            .set(state.ids.skills[widget_index], ui);
2915
2916        // Lock Image
2917        if locked {
2918            Image::new(self.imgs.lock)
2919                .w_h(76.0, 76.0)
2920                .middle_of(state.ids.skills[widget_index])
2921                .graphics_for(state.ids.skills[widget_index])
2922                .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.8)))
2923                .set(state.ids.skill_lock_imgs[widget_index], ui);
2924        }
2925
2926        if button.was_clicked() {
2927            events.push(Event::UnlockSkill(skill));
2928        };
2929    }
2930}
2931
2932/// Returns skill info as a tuple of title and description.
2933///
2934/// If you want to get localized version, use `SkillStrings::localize` method
2935fn skill_strings(skill: Skill) -> SkillStrings<'static> {
2936    match skill {
2937        // general tree
2938        Skill::UnlockGroup(s) => unlock_skill_strings(s),
2939        // weapon trees
2940        Skill::Sceptre(s) => sceptre_skill_strings(s),
2941        // movement trees
2942        Skill::Climb(s) => climb_skill_strings(s),
2943        Skill::Swim(s) => swim_skill_strings(s),
2944        // mining
2945        Skill::Pick(s) => mining_skill_strings(s),
2946        _ => SkillStrings::plain("", ""),
2947    }
2948}
2949
2950fn unlock_skill_strings(group: SkillGroupKind) -> SkillStrings<'static> {
2951    match group {
2952        SkillGroupKind::Weapon(ToolKind::Sword) => {
2953            SkillStrings::plain("hud-skill-unlck_sword_title", "hud-skill-unlck_sword")
2954        },
2955        SkillGroupKind::Weapon(ToolKind::Axe) => {
2956            SkillStrings::plain("hud-skill-unlck_axe_title", "hud-skill-unlck_axe")
2957        },
2958        SkillGroupKind::Weapon(ToolKind::Hammer) => {
2959            SkillStrings::plain("hud-skill-unlck_hammer_title", "hud-skill-unlck_hammer")
2960        },
2961        SkillGroupKind::Weapon(ToolKind::Bow) => {
2962            SkillStrings::plain("hud-skill-unlck_bow_title", "hud-skill-unlck_bow")
2963        },
2964        SkillGroupKind::Weapon(ToolKind::Staff) => {
2965            SkillStrings::plain("hud-skill-unlck_staff_title", "hud-skill-unlck_staff")
2966        },
2967        SkillGroupKind::Weapon(ToolKind::Sceptre) => {
2968            SkillStrings::plain("hud-skill-unlck_sceptre_title", "hud-skill-unlck_sceptre")
2969        },
2970        SkillGroupKind::General
2971        | SkillGroupKind::Weapon(
2972            ToolKind::Dagger
2973            | ToolKind::Shield
2974            | ToolKind::Spear
2975            | ToolKind::Blowgun
2976            | ToolKind::Debug
2977            | ToolKind::Farming
2978            | ToolKind::Instrument
2979            | ToolKind::Throwable
2980            | ToolKind::Pick
2981            | ToolKind::Shovel
2982            | ToolKind::Natural
2983            | ToolKind::Empty,
2984        ) => {
2985            tracing::warn!("Requesting title for unlocking unexpected skill group");
2986            SkillStrings::Empty
2987        },
2988    }
2989}
2990
2991fn sceptre_skill_strings(skill: SceptreSkill) -> SkillStrings<'static> {
2992    let modifiers = SKILL_MODIFIERS.sceptre_tree;
2993    match skill {
2994        // Lifesteal beam upgrades
2995        SceptreSkill::LDamage => SkillStrings::with_mult(
2996            "hud-skill-sc_lifesteal_damage_title",
2997            "hud-skill-sc_lifesteal_damage",
2998            modifiers.beam.damage,
2999        ),
3000        SceptreSkill::LRange => SkillStrings::with_mult(
3001            "hud-skill-sc_lifesteal_range_title",
3002            "hud-skill-sc_lifesteal_range",
3003            modifiers.beam.range,
3004        ),
3005        SceptreSkill::LLifesteal => SkillStrings::with_mult(
3006            "hud-skill-sc_lifesteal_lifesteal_title",
3007            "hud-skill-sc_lifesteal_lifesteal",
3008            modifiers.beam.lifesteal,
3009        ),
3010        SceptreSkill::LRegen => SkillStrings::with_mult(
3011            "hud-skill-sc_lifesteal_regen_title",
3012            "hud-skill-sc_lifesteal_regen",
3013            modifiers.beam.energy_regen,
3014        ),
3015        // Healing aura upgrades
3016        SceptreSkill::HHeal => SkillStrings::with_mult(
3017            "hud-skill-sc_heal_heal_title",
3018            "hud-skill-sc_heal_heal",
3019            modifiers.healing_aura.strength,
3020        ),
3021        SceptreSkill::HRange => SkillStrings::with_mult(
3022            "hud-skill-sc_heal_range_title",
3023            "hud-skill-sc_heal_range",
3024            modifiers.healing_aura.range,
3025        ),
3026        SceptreSkill::HDuration => SkillStrings::with_mult(
3027            "hud-skill-sc_heal_duration_title",
3028            "hud-skill-sc_heal_duration",
3029            modifiers.healing_aura.duration,
3030        ),
3031        SceptreSkill::HCost => SkillStrings::with_mult(
3032            "hud-skill-sc_heal_cost_title",
3033            "hud-skill-sc_heal_cost",
3034            modifiers.healing_aura.energy_cost,
3035        ),
3036        // Warding aura upgrades
3037        SceptreSkill::UnlockAura => SkillStrings::plain(
3038            "hud-skill-sc_wardaura_unlock_title",
3039            "hud-skill-sc_wardaura_unlock",
3040        ),
3041        SceptreSkill::AStrength => SkillStrings::with_mult(
3042            "hud-skill-sc_wardaura_strength_title",
3043            "hud-skill-sc_wardaura_strength",
3044            modifiers.warding_aura.strength,
3045        ),
3046        SceptreSkill::ADuration => SkillStrings::with_mult(
3047            "hud-skill-sc_wardaura_duration_title",
3048            "hud-skill-sc_wardaura_duration",
3049            modifiers.warding_aura.duration,
3050        ),
3051        SceptreSkill::ARange => SkillStrings::with_mult(
3052            "hud-skill-sc_wardaura_range_title",
3053            "hud-skill-sc_wardaura_range",
3054            modifiers.warding_aura.range,
3055        ),
3056        SceptreSkill::ACost => SkillStrings::with_mult(
3057            "hud-skill-sc_wardaura_cost_title",
3058            "hud-skill-sc_wardaura_cost",
3059            modifiers.warding_aura.energy_cost,
3060        ),
3061    }
3062}
3063
3064fn climb_skill_strings(skill: ClimbSkill) -> SkillStrings<'static> {
3065    let modifiers = SKILL_MODIFIERS.general_tree.climb;
3066    match skill {
3067        ClimbSkill::Cost => SkillStrings::with_mult(
3068            "hud-skill-climbing_cost_title",
3069            "hud-skill-climbing_cost",
3070            modifiers.energy_cost,
3071        ),
3072        ClimbSkill::Speed => SkillStrings::with_mult(
3073            "hud-skill-climbing_speed_title",
3074            "hud-skill-climbing_speed",
3075            modifiers.speed,
3076        ),
3077    }
3078}
3079
3080fn swim_skill_strings(skill: SwimSkill) -> SkillStrings<'static> {
3081    let modifiers = SKILL_MODIFIERS.general_tree.swim;
3082    match skill {
3083        SwimSkill::Speed => SkillStrings::with_mult(
3084            "hud-skill-swim_speed_title",
3085            "hud-skill-swim_speed",
3086            modifiers.speed,
3087        ),
3088    }
3089}
3090
3091fn mining_skill_strings(skill: MiningSkill) -> SkillStrings<'static> {
3092    let modifiers = SKILL_MODIFIERS.mining_tree;
3093    match skill {
3094        MiningSkill::Speed => SkillStrings::with_mult(
3095            "hud-skill-pick_strike_speed_title",
3096            "hud-skill-pick_strike_speed",
3097            modifiers.speed,
3098        ),
3099        MiningSkill::OreGain => SkillStrings::with_const(
3100            "hud-skill-pick_strike_oregain_title",
3101            "hud-skill-pick_strike_oregain",
3102            (modifiers.ore_gain * 100.0).round() as u32,
3103        ),
3104        MiningSkill::GemGain => SkillStrings::with_const(
3105            "hud-skill-pick_strike_gemgain_title",
3106            "hud-skill-pick_strike_gemgain",
3107            (modifiers.gem_gain * 100.0).round() as u32,
3108        ),
3109    }
3110}
3111
3112/// Helper object used returned by `skill_strings` as source for
3113/// later internationalization and formatting.
3114enum SkillStrings<'a> {
3115    Plain {
3116        title: &'a str,
3117        desc: &'a str,
3118    },
3119    WithConst {
3120        title: &'a str,
3121        desc: &'a str,
3122        constant: u32,
3123    },
3124    WithMult {
3125        title: &'a str,
3126        desc: &'a str,
3127        multiplier: f32,
3128    },
3129    Empty,
3130}
3131
3132impl<'a> SkillStrings<'a> {
3133    fn plain(title: &'a str, desc: &'a str) -> Self { Self::Plain { title, desc } }
3134
3135    fn with_const(title: &'a str, desc: &'a str, constant: u32) -> Self {
3136        Self::WithConst {
3137            title,
3138            desc,
3139            constant,
3140        }
3141    }
3142
3143    fn with_mult(title: &'a str, desc: &'a str, multiplier: f32) -> Self {
3144        Self::WithMult {
3145            title,
3146            desc,
3147            multiplier,
3148        }
3149    }
3150
3151    fn localize<'loc>(
3152        &self,
3153        i18n: &'loc Localization,
3154        skill_set: &SkillSet,
3155        skill: Skill,
3156    ) -> (Cow<'loc, str>, Cow<'loc, str>) {
3157        match self {
3158            Self::Plain { title, desc } => {
3159                let title = i18n.get_msg(title);
3160
3161                let args = i18n::fluent_args! {
3162                    "SP" => sp(i18n, skill_set, skill),
3163                };
3164                let desc = i18n.get_msg_ctx(desc, &args);
3165
3166                (title, desc)
3167            },
3168            Self::WithConst {
3169                title,
3170                desc,
3171                constant,
3172            } => {
3173                let title = i18n.get_msg(title);
3174                let args = i18n::fluent_args! {
3175                    "boost" => constant,
3176                    "SP" => sp(i18n, skill_set, skill),
3177                };
3178                let desc = i18n.get_msg_ctx(desc, &args);
3179
3180                (title, desc)
3181            },
3182            Self::WithMult {
3183                title,
3184                desc,
3185                multiplier,
3186            } => {
3187                let percentage = hud::multiplier_to_percentage(*multiplier).abs();
3188
3189                let title = i18n.get_msg(title);
3190
3191                let args = i18n::fluent_args! {
3192                    "boost" => format!("{percentage:.0}"),
3193                    "SP" => sp(i18n, skill_set, skill),
3194                };
3195                let desc = i18n.get_msg_ctx(desc, &args);
3196
3197                (title, desc)
3198            },
3199            Self::Empty => (Cow::Borrowed(""), Cow::Borrowed("")),
3200        }
3201    }
3202}
3203
3204/// The number of variants of the [`CharacterStat`] enum.
3205const STAT_COUNT: usize = 15;
3206
3207#[derive(EnumIter)]
3208enum CharacterStat {
3209    Name,
3210    BattleMode,
3211    Waypoint,
3212    Hitpoints,
3213    Energy,
3214    Poise,
3215    CombatRating,
3216    Protection,
3217    StunResistance,
3218    PrecisionPower,
3219    EnergyReward,
3220    Stealth,
3221    WeaponPower,
3222    WeaponSpeed,
3223    WeaponEffectPower,
3224}
3225
3226impl CharacterStat {
3227    fn localized_str<'a>(&self, i18n: &'a Localization) -> Cow<'a, str> {
3228        use CharacterStat::*;
3229
3230        match self {
3231            Name => i18n.get_msg("character_window-character_name"),
3232            BattleMode => i18n.get_msg("hud-battle-mode"),
3233            Waypoint => i18n.get_msg("hud-waypoint"),
3234            Hitpoints => i18n.get_msg("hud-bag-health"),
3235            Energy => i18n.get_msg("hud-bag-energy"),
3236            CombatRating => i18n.get_msg("hud-bag-combat_rating"),
3237            Protection => i18n.get_msg("hud-bag-protection"),
3238            StunResistance => i18n.get_msg("hud-bag-stun_res"),
3239            Poise => i18n.get_msg("common-stats-poise_res"),
3240            PrecisionPower => i18n.get_msg("common-stats-precision_power"),
3241            EnergyReward => i18n.get_msg("common-stats-energy_reward"),
3242            Stealth => i18n.get_msg("common-stats-stealth"),
3243            WeaponPower => i18n.get_msg("common-stats-power"),
3244            WeaponSpeed => i18n.get_msg("common-stats-speed"),
3245            WeaponEffectPower => i18n.get_msg("common-stats-effect-power"),
3246        }
3247    }
3248}
3249
3250fn sp<'loc>(i18n: &'loc Localization, skill_set: &SkillSet, skill: Skill) -> Cow<'loc, str> {
3251    let current_level = skill_set.skill_level(skill);
3252    if matches!(current_level, Ok(level) if level == skill.max_level()) {
3253        Cow::Borrowed("")
3254    } else {
3255        i18n.get_msg_ctx("hud-skill-req_sp", &i18n::fluent_args! {
3256            "number" => skill_set.skill_cost(skill),
3257        })
3258    }
3259}