Skip to main content

veloren_voxygen/hud/
diary.rs

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