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