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