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::{self, formatted_float, formatted_float_signed},
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                    global_state: self.global_state,
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                    global_state: self.global_state,
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                            formatted_float(cr * 10.0, 2)
1267                        },
1268                        CharacterStat::Protection => {
1269                            let protection =
1270                                combat::compute_protection(Some(self.inventory), self.msm);
1271                            match protection {
1272                                Some(prot) => formatted_float(prot, 2),
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!("{}%", formatted_float(stun_res * 100.0, 2))
1284                        },
1285                        CharacterStat::PrecisionPower => {
1286                            let precision_power =
1287                                combat::compute_precision_mult(Some(self.inventory), self.msm);
1288                            format!("x{}", formatted_float(precision_power, 2))
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 = format!(
1303                                "{}%",
1304                                formatted_float_signed(
1305                                    (1.0 - stealth_perception_multiplier) * 100.0,
1306                                    1,
1307                                )
1308                            );
1309
1310                            txt
1311                        },
1312                        CharacterStat::WeaponPower => match (main_weap_stats, off_weap_stats) {
1313                            (Some(m_stats), Some(o_stats)) => {
1314                                format!(
1315                                    "{}   {}",
1316                                    formatted_float(m_stats.power * 10.0, 2),
1317                                    formatted_float(o_stats.power * 10.0, 2),
1318                                )
1319                            },
1320                            (Some(stats), None) | (None, Some(stats)) => {
1321                                formatted_float(stats.power * 10.0, 2)
1322                            },
1323                            (None, None) => String::new(),
1324                        },
1325                        CharacterStat::WeaponSpeed => {
1326                            let spd_fmt = |sp| (sp - 1.0) * 100.0;
1327                            match (main_weap_stats, off_weap_stats) {
1328                                (Some(m_stats), Some(o_stats)) => format!(
1329                                    "{:+.0}%   {:+.0}%",
1330                                    spd_fmt(m_stats.speed),
1331                                    spd_fmt(o_stats.speed)
1332                                ),
1333                                (Some(stats), None) | (None, Some(stats)) => {
1334                                    format!("{:+.0}%", spd_fmt(stats.speed))
1335                                },
1336                                _ => String::new(),
1337                            }
1338                        },
1339                        CharacterStat::WeaponEffectPower => match (main_weap_stats, off_weap_stats)
1340                        {
1341                            (Some(m_stats), Some(o_stats)) => {
1342                                format!(
1343                                    "{}   {}",
1344                                    formatted_float(m_stats.effect_power * 10.0, 2),
1345                                    formatted_float(o_stats.effect_power * 10.0, 2),
1346                                )
1347                            },
1348                            (Some(stats), None) | (None, Some(stats)) => {
1349                                formatted_float(stats.effect_power * 10.0, 2)
1350                            },
1351                            (None, None) => String::new(),
1352                        },
1353                    };
1354
1355                    let mut number = Text::new(&value)
1356                        .font_id(self.fonts.cyri.conrod_id)
1357                        .font_size(self.fonts.cyri.scale(29))
1358                        .color(BLACK);
1359
1360                    if i == 0 {
1361                        number = number.right_from(state.ids.stat_names[i], 165.0);
1362                    } else {
1363                        number = number.down_from(state.ids.stat_values[i - 1], 10.0);
1364                    };
1365                    number.set(state.ids.stat_values[i], ui);
1366                }
1367
1368                events
1369            },
1370            DiarySection::Recipes => {
1371                // Background Art
1372                Image::new(self.imgs.book_bg)
1373                    .w_h(299.0 * 4.0, 184.0 * 4.0)
1374                    .mid_top_with_margin_on(state.ids.content_align, 4.0)
1375                    .set(state.ids.spellbook_art, ui);
1376
1377                Rectangle::fill_with([299.0 * 2.0, 184.0 * 4.0], color::TRANSPARENT)
1378                    .top_left_with_margins_on(state.ids.spellbook_art, 0.0, 0.0)
1379                    .set(state.ids.sb_page_left_align, ui);
1380                Rectangle::fill_with([299.0 * 2.0, 184.0 * 4.0], color::TRANSPARENT)
1381                    .top_right_with_margins_on(state.ids.spellbook_art, 0.0, 0.0)
1382                    .set(state.ids.sb_page_right_align, ui);
1383
1384                const RECIPES_PER_PAGE: usize = 36;
1385
1386                let page_index_max =
1387                    self.inventory.recipe_groups_iter().len().saturating_sub(1) / RECIPES_PER_PAGE;
1388
1389                if state.recipe_page > page_index_max {
1390                    state.update(|s| s.recipe_page = 0);
1391                }
1392
1393                // Page button
1394                // Left Arrow
1395                let left_arrow = Button::image(if state.recipe_page > 0 {
1396                    self.imgs.arrow_l
1397                } else {
1398                    self.imgs.arrow_l_inactive
1399                })
1400                .bottom_left_with_margins_on(state.ids.spellbook_art, -83.0, 10.0)
1401                .w_h(48.0, 55.0);
1402                // Grey out arrows when inactive
1403                if state.recipe_page > 0 {
1404                    if left_arrow
1405                        .hover_image(self.imgs.arrow_l_click)
1406                        .press_image(self.imgs.arrow_l)
1407                        .set(state.ids.ability_page_left, ui)
1408                        .was_clicked()
1409                    {
1410                        state.update(|s| s.recipe_page -= 1);
1411                    }
1412                } else {
1413                    left_arrow.set(state.ids.ability_page_left, ui);
1414                }
1415                // Right Arrow
1416                let right_arrow = Button::image(if state.recipe_page < page_index_max {
1417                    self.imgs.arrow_r
1418                } else {
1419                    self.imgs.arrow_r_inactive
1420                })
1421                .bottom_right_with_margins_on(state.ids.spellbook_art, -83.0, 10.0)
1422                .w_h(48.0, 55.0);
1423                if state.recipe_page < page_index_max {
1424                    // Only show right button if not on last page
1425                    if right_arrow
1426                        .hover_image(self.imgs.arrow_r_click)
1427                        .press_image(self.imgs.arrow_r)
1428                        .set(state.ids.ability_page_right, ui)
1429                        .was_clicked()
1430                    {
1431                        state.update(|s| s.recipe_page += 1);
1432                    };
1433                } else {
1434                    right_arrow.set(state.ids.ability_page_right, ui);
1435                }
1436
1437                state.update(|s| {
1438                    s.ids
1439                        .recipe_groups
1440                        .resize(RECIPES_PER_PAGE, &mut ui.widget_id_generator())
1441                });
1442
1443                for (i, rg) in self
1444                    .inventory
1445                    .recipe_groups_iter()
1446                    .skip(state.recipe_page * RECIPES_PER_PAGE)
1447                    .take(RECIPES_PER_PAGE)
1448                    .enumerate()
1449                {
1450                    let (title, _desc) =
1451                        util::item_text(rg, self.localized_strings, self.item_i18n);
1452
1453                    let mut text = Text::new(&title)
1454                        .font_id(self.fonts.cyri.conrod_id)
1455                        .font_size(self.fonts.cyri.scale(29))
1456                        .color(BLACK);
1457
1458                    if i == 0 {
1459                        text =
1460                            text.top_left_with_margins_on(state.ids.sb_page_left_align, 20.0, 20.0);
1461                    } else if i == 18 {
1462                        text = text.top_left_with_margins_on(
1463                            state.ids.sb_page_right_align,
1464                            20.0,
1465                            20.0,
1466                        );
1467                    } else {
1468                        text = text.down_from(state.ids.recipe_groups[i - 1], 10.0);
1469                    }
1470                    text.set(state.ids.recipe_groups[i], ui);
1471                }
1472
1473                events
1474            },
1475        }
1476    }
1477}
1478
1479enum SkillIcon<'a> {
1480    Unlockable {
1481        skill: Skill,
1482        image: image::Id,
1483        position: PositionSpecifier,
1484        id: widget::Id,
1485    },
1486    Descriptive {
1487        title: &'a str,
1488        desc: &'a str,
1489        image: image::Id,
1490        position: PositionSpecifier,
1491        id: widget::Id,
1492    },
1493    Ability {
1494        skill: Skill,
1495        ability_id: &'a str,
1496        position: PositionSpecifier,
1497    },
1498}
1499
1500impl Diary<'_> {
1501    fn handle_general_skills_window(
1502        &mut self,
1503        diary_tooltip: &Tooltip,
1504        state: &mut State<DiaryState>,
1505        ui: &mut UiCell,
1506        mut events: Vec<Event>,
1507    ) -> Vec<Event> {
1508        let tree_title = &self.localized_strings.get_msg("common-weapons-general");
1509        Text::new(tree_title)
1510            .mid_top_with_margin_on(state.ids.content_align, 2.0)
1511            .font_id(self.fonts.cyri.conrod_id)
1512            .font_size(self.fonts.cyri.scale(34))
1513            .color(TEXT_COLOR)
1514            .set(state.ids.tree_title_txt, ui);
1515
1516        // Number of skills per rectangle per weapon, start counting at 0
1517        // Maximum of 9 skills/8 indices
1518        let skills_top_l = 6;
1519        let skills_top_r = 0;
1520        let skills_bot_l = 0;
1521        let skills_bot_r = 5;
1522
1523        self.setup_state_for_skill_icons(
1524            state,
1525            ui,
1526            skills_top_l,
1527            skills_top_r,
1528            skills_bot_l,
1529            skills_bot_r,
1530        );
1531
1532        use SkillGroupKind::*;
1533        use ToolKind::*;
1534        // General Combat
1535        Image::new(animate_by_pulse(
1536            &self.item_imgs.img_ids_or_not_found_img(ItemKey::Simple(
1537                "example_general_combat_left".to_string(),
1538            )),
1539            self.pulse,
1540        ))
1541        .wh(ART_SIZE)
1542        .middle_of(state.ids.content_align)
1543        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1544        .set(state.ids.general_combat_render_0, ui);
1545
1546        Image::new(animate_by_pulse(
1547            &self.item_imgs.img_ids_or_not_found_img(ItemKey::Simple(
1548                "example_general_combat_right".to_string(),
1549            )),
1550            self.pulse,
1551        ))
1552        .wh(ART_SIZE)
1553        .middle_of(state.ids.general_combat_render_0)
1554        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1555        .set(state.ids.general_combat_render_1, ui);
1556
1557        use PositionSpecifier::MidTopWithMarginOn;
1558        let skill_buttons = &[
1559            // Top Left skills
1560            //        5 1 6
1561            //        3 0 4
1562            //        8 2 7
1563            // Bottom left skills
1564            SkillIcon::Unlockable {
1565                skill: Skill::UnlockGroup(Weapon(Sword)),
1566                image: self.imgs.unlock_sword_skill,
1567                position: MidTopWithMarginOn(state.ids.skills_top_l[0], 3.0),
1568                id: state.ids.skill_general_tree_0,
1569            },
1570            SkillIcon::Unlockable {
1571                skill: Skill::UnlockGroup(Weapon(Axe)),
1572                image: self.imgs.unlock_axe_skill,
1573                position: MidTopWithMarginOn(state.ids.skills_top_l[1], 3.0),
1574                id: state.ids.skill_general_tree_1,
1575            },
1576            SkillIcon::Unlockable {
1577                skill: Skill::UnlockGroup(Weapon(Hammer)),
1578                image: self.imgs.unlock_hammer_skill,
1579                position: MidTopWithMarginOn(state.ids.skills_top_l[2], 3.0),
1580                id: state.ids.skill_general_tree_2,
1581            },
1582            SkillIcon::Unlockable {
1583                skill: Skill::UnlockGroup(Weapon(Bow)),
1584                image: self.imgs.unlock_bow_skill,
1585                position: MidTopWithMarginOn(state.ids.skills_top_l[3], 3.0),
1586                id: state.ids.skill_general_tree_3,
1587            },
1588            SkillIcon::Unlockable {
1589                skill: Skill::UnlockGroup(Weapon(Staff)),
1590                image: self.imgs.unlock_staff_skill0,
1591                position: MidTopWithMarginOn(state.ids.skills_top_l[4], 3.0),
1592                id: state.ids.skill_general_tree_4,
1593            },
1594            SkillIcon::Unlockable {
1595                skill: Skill::UnlockGroup(Weapon(Sceptre)),
1596                image: self.imgs.unlock_sceptre_skill,
1597                position: MidTopWithMarginOn(state.ids.skills_top_l[5], 3.0),
1598                id: state.ids.skill_general_tree_5,
1599            },
1600            // Bottom right skills
1601            SkillIcon::Descriptive {
1602                title: "hud-skill-climbing_title",
1603                desc: "hud-skill-climbing",
1604                image: self.imgs.skill_climbing_skill,
1605                position: MidTopWithMarginOn(state.ids.skills_bot_r[0], 3.0),
1606                id: state.ids.skill_general_climb_0,
1607            },
1608            SkillIcon::Unlockable {
1609                skill: Skill::Climb(ClimbSkill::Cost),
1610                image: self.imgs.utility_cost_skill,
1611                position: MidTopWithMarginOn(state.ids.skills_bot_r[1], 3.0),
1612                id: state.ids.skill_general_climb_1,
1613            },
1614            SkillIcon::Unlockable {
1615                skill: Skill::Climb(ClimbSkill::Speed),
1616                image: self.imgs.utility_speed_skill,
1617                position: MidTopWithMarginOn(state.ids.skills_bot_r[2], 3.0),
1618                id: state.ids.skill_general_climb_2,
1619            },
1620            SkillIcon::Descriptive {
1621                title: "hud-skill-swim_title",
1622                desc: "hud-skill-swim",
1623                image: self.imgs.skill_swim_skill,
1624                position: MidTopWithMarginOn(state.ids.skills_bot_r[3], 3.0),
1625                id: state.ids.skill_general_swim_0,
1626            },
1627            SkillIcon::Unlockable {
1628                skill: Skill::Swim(SwimSkill::Speed),
1629                image: self.imgs.utility_speed_skill,
1630                position: MidTopWithMarginOn(state.ids.skills_bot_r[4], 3.0),
1631                id: state.ids.skill_general_swim_1,
1632            },
1633        ];
1634
1635        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
1636        events
1637    }
1638
1639    fn handle_sword_skills_window(
1640        &mut self,
1641        diary_tooltip: &Tooltip,
1642        state: &mut State<DiaryState>,
1643        ui: &mut UiCell,
1644        mut events: Vec<Event>,
1645    ) -> Vec<Event> {
1646        Image::new(self.imgs.sword_tree_paths)
1647            .wh([1042.0, 636.0])
1648            .mid_top_with_margin_on(state.ids.content_align, 55.0)
1649            .graphics_for(state.ids.content_align)
1650            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1651            .set(state.ids.sword_path_overlay, ui);
1652
1653        // Sword
1654        Image::new(self.imgs.sword_bg)
1655            .wh([933.0, 615.0])
1656            .mid_top_with_margin_on(state.ids.content_align, 65.0)
1657            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1658            .set(state.ids.sword_bg, ui);
1659
1660        // Stances:
1661        // For alignment purposes
1662        Rectangle::fill_with([169.0, 615.0], color::TRANSPARENT)
1663            .top_left_of(state.ids.sword_bg)
1664            .set(state.ids.sword_stance_left_align, ui);
1665        Rectangle::fill_with([169.0, 615.0], color::TRANSPARENT)
1666            .top_right_of(state.ids.sword_bg)
1667            .set(state.ids.sword_stance_right_align, ui);
1668
1669        // Cleaving
1670        Text::new(
1671            &self
1672                .localized_strings
1673                .get_msg("hud-skill-sword_stance_cleaving"),
1674        )
1675        .mid_top_with_margin_on(state.ids.sword_stance_left_align, -7.0)
1676        .font_id(self.fonts.cyri.conrod_id)
1677        .font_size(self.fonts.cyri.scale(34))
1678        .color(Color::Rgba(0.94, 0.54, 0.07, 1.0))
1679        .set(state.ids.sword_stance_cleaving_text, ui);
1680
1681        Text::new(
1682            &self
1683                .localized_strings
1684                .get_msg("hud-skill-sword_stance_cleaving"),
1685        )
1686        .x_y_position_relative_to(
1687            state.ids.sword_stance_cleaving_text,
1688            Relative::Scalar(2.5),
1689            Relative::Scalar(-2.5),
1690        )
1691        .depth(1.0)
1692        .font_id(self.fonts.cyri.conrod_id)
1693        .font_size(self.fonts.cyri.scale(34))
1694        .color(color::BLACK)
1695        .set(state.ids.sword_stance_cleaving_shadow, ui);
1696
1697        // Agile
1698        Text::new(
1699            &self
1700                .localized_strings
1701                .get_msg("hud-skill-sword_stance_agile"),
1702        )
1703        .mid_top_with_margin_on(state.ids.sword_bg, -7.0)
1704        .font_id(self.fonts.cyri.conrod_id)
1705        .font_size(self.fonts.cyri.scale(34))
1706        .color(Color::Rgba(0.81, 0.70, 0.08, 1.0))
1707        .set(state.ids.sword_stance_agile_text, ui);
1708
1709        Text::new(
1710            &self
1711                .localized_strings
1712                .get_msg("hud-skill-sword_stance_agile"),
1713        )
1714        .x_y_position_relative_to(
1715            state.ids.sword_stance_agile_text,
1716            Relative::Scalar(2.5),
1717            Relative::Scalar(-2.5),
1718        )
1719        .depth(1.0)
1720        .font_id(self.fonts.cyri.conrod_id)
1721        .font_size(self.fonts.cyri.scale(34))
1722        .color(color::BLACK)
1723        .set(state.ids.sword_stance_agile_shadow, ui);
1724
1725        // Crippling
1726        Text::new(
1727            &self
1728                .localized_strings
1729                .get_msg("hud-skill-sword_stance_crippling"),
1730        )
1731        .mid_top_with_margin_on(state.ids.sword_stance_right_align, -7.0)
1732        .font_id(self.fonts.cyri.conrod_id)
1733        .font_size(self.fonts.cyri.scale(34))
1734        .color(Color::Rgba(0.0, 0.52, 0.0, 1.0))
1735        .set(state.ids.sword_stance_crippling_text, ui);
1736
1737        Text::new(
1738            &self
1739                .localized_strings
1740                .get_msg("hud-skill-sword_stance_crippling"),
1741        )
1742        .x_y_position_relative_to(
1743            state.ids.sword_stance_crippling_text,
1744            Relative::Scalar(2.5),
1745            Relative::Scalar(-2.5),
1746        )
1747        .depth(1.0)
1748        .font_id(self.fonts.cyri.conrod_id)
1749        .font_size(self.fonts.cyri.scale(34))
1750        .color(color::BLACK)
1751        .set(state.ids.sword_stance_crippling_shadow, ui);
1752
1753        // Heavy
1754        Text::new(
1755            &self
1756                .localized_strings
1757                .get_msg("hud-skill-sword_stance_heavy"),
1758        )
1759        .mid_bottom_with_margin_on(state.ids.sword_stance_left_align, 272.0)
1760        .font_id(self.fonts.cyri.conrod_id)
1761        .font_size(self.fonts.cyri.scale(34))
1762        .color(Color::Rgba(0.67, 0.0, 0.0, 1.0))
1763        .set(state.ids.sword_stance_heavy_text, ui);
1764
1765        Text::new(
1766            &self
1767                .localized_strings
1768                .get_msg("hud-skill-sword_stance_heavy"),
1769        )
1770        .x_y_position_relative_to(
1771            state.ids.sword_stance_heavy_text,
1772            Relative::Scalar(2.5),
1773            Relative::Scalar(-2.5),
1774        )
1775        .depth(1.0)
1776        .font_id(self.fonts.cyri.conrod_id)
1777        .font_size(self.fonts.cyri.scale(34))
1778        .color(color::BLACK)
1779        .set(state.ids.sword_stance_heavy_shadow, ui);
1780
1781        // Defensive
1782        Text::new(
1783            &self
1784                .localized_strings
1785                .get_msg("hud-skill-sword_stance_defensive"),
1786        )
1787        .mid_bottom_with_margin_on(state.ids.sword_stance_right_align, 272.0)
1788        .font_id(self.fonts.cyri.conrod_id)
1789        .font_size(self.fonts.cyri.scale(34))
1790        .color(Color::Rgba(0.10, 0.40, 0.82, 1.0))
1791        .set(state.ids.sword_stance_defensive_text, ui);
1792
1793        Text::new(
1794            &self
1795                .localized_strings
1796                .get_msg("hud-skill-sword_stance_defensive"),
1797        )
1798        .x_y_position_relative_to(
1799            state.ids.sword_stance_defensive_text,
1800            Relative::Scalar(2.5),
1801            Relative::Scalar(-2.5),
1802        )
1803        .depth(1.0)
1804        .font_id(self.fonts.cyri.conrod_id)
1805        .font_size(self.fonts.cyri.scale(34))
1806        .color(color::BLACK)
1807        .set(state.ids.sword_stance_defensive_shadow, ui);
1808
1809        use PositionSpecifier::TopLeftWithMarginsOn;
1810        let skill_buttons = &[
1811            SkillIcon::Ability {
1812                skill: Skill::Sword(SwordSkill::CrescentSlash),
1813                ability_id: "veloren.core.pseudo_abilities.sword.crescent_slash",
1814                position: TopLeftWithMarginsOn(state.ids.sword_bg, 537.0, 429.0),
1815            },
1816            SkillIcon::Ability {
1817                skill: Skill::Sword(SwordSkill::FellStrike),
1818                ability_id: "veloren.core.pseudo_abilities.sword.fell_strike",
1819                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 527.0),
1820            },
1821            SkillIcon::Ability {
1822                skill: Skill::Sword(SwordSkill::Skewer),
1823                ability_id: "veloren.core.pseudo_abilities.sword.skewer",
1824                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 527.0),
1825            },
1826            SkillIcon::Ability {
1827                skill: Skill::Sword(SwordSkill::Cascade),
1828                ability_id: "veloren.core.pseudo_abilities.sword.cascade",
1829                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 332.0),
1830            },
1831            SkillIcon::Ability {
1832                skill: Skill::Sword(SwordSkill::CrossCut),
1833                ability_id: "veloren.core.pseudo_abilities.sword.cross_cut",
1834                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 332.0),
1835            },
1836            SkillIcon::Ability {
1837                skill: Skill::Sword(SwordSkill::Finisher),
1838                ability_id: "veloren.core.pseudo_abilities.sword.finisher",
1839                position: TopLeftWithMarginsOn(state.ids.sword_bg, 263.0, 429.0),
1840            },
1841            SkillIcon::Ability {
1842                skill: Skill::Sword(SwordSkill::HeavySweep),
1843                ability_id: "common.abilities.sword.heavy_sweep",
1844                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 2.0),
1845            },
1846            SkillIcon::Ability {
1847                skill: Skill::Sword(SwordSkill::HeavyPommelStrike),
1848                ability_id: "common.abilities.sword.heavy_pommel_strike",
1849                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 91.0),
1850            },
1851            SkillIcon::Ability {
1852                skill: Skill::Sword(SwordSkill::AgileQuickDraw),
1853                ability_id: "common.abilities.sword.agile_quick_draw",
1854                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 384.0),
1855            },
1856            SkillIcon::Ability {
1857                skill: Skill::Sword(SwordSkill::AgileFeint),
1858                ability_id: "common.abilities.sword.agile_feint",
1859                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 472.0),
1860            },
1861            SkillIcon::Ability {
1862                skill: Skill::Sword(SwordSkill::DefensiveRiposte),
1863                ability_id: "common.abilities.sword.defensive_riposte",
1864                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 766.0),
1865            },
1866            SkillIcon::Ability {
1867                skill: Skill::Sword(SwordSkill::DefensiveDisengage),
1868                ability_id: "common.abilities.sword.defensive_disengage",
1869                position: TopLeftWithMarginsOn(state.ids.sword_bg, 457.0, 855.0),
1870            },
1871            SkillIcon::Ability {
1872                skill: Skill::Sword(SwordSkill::CripplingGouge),
1873                ability_id: "common.abilities.sword.crippling_gouge",
1874                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 766.0),
1875            },
1876            SkillIcon::Ability {
1877                skill: Skill::Sword(SwordSkill::CripplingHamstring),
1878                ability_id: "common.abilities.sword.crippling_hamstring",
1879                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 766.0),
1880            },
1881            SkillIcon::Ability {
1882                skill: Skill::Sword(SwordSkill::CleavingWhirlwindSlice),
1883                ability_id: "common.abilities.sword.cleaving_whirlwind_slice",
1884                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 91.0),
1885            },
1886            SkillIcon::Ability {
1887                skill: Skill::Sword(SwordSkill::CleavingEarthSplitter),
1888                ability_id: "common.abilities.sword.cleaving_earth_splitter",
1889                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 91.0),
1890            },
1891            SkillIcon::Ability {
1892                skill: Skill::Sword(SwordSkill::HeavyFortitude),
1893                ability_id: "common.abilities.sword.heavy_fortitude",
1894                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 2.0),
1895            },
1896            SkillIcon::Ability {
1897                skill: Skill::Sword(SwordSkill::HeavyPillarThrust),
1898                ability_id: "common.abilities.sword.heavy_pillar_thrust",
1899                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 91.0),
1900            },
1901            SkillIcon::Ability {
1902                skill: Skill::Sword(SwordSkill::AgileDancingEdge),
1903                ability_id: "common.abilities.sword.agile_dancing_edge",
1904                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 385.0),
1905            },
1906            SkillIcon::Ability {
1907                skill: Skill::Sword(SwordSkill::AgileFlurry),
1908                ability_id: "common.abilities.sword.agile_flurry",
1909                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 473.0),
1910            },
1911            SkillIcon::Ability {
1912                skill: Skill::Sword(SwordSkill::DefensiveStalwartSword),
1913                ability_id: "common.abilities.sword.defensive_stalwart_sword",
1914                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 766.0),
1915            },
1916            SkillIcon::Ability {
1917                skill: Skill::Sword(SwordSkill::DefensiveDeflect),
1918                ability_id: "common.abilities.sword.defensive_deflect",
1919                position: TopLeftWithMarginsOn(state.ids.sword_bg, 368.0, 855.0),
1920            },
1921            SkillIcon::Ability {
1922                skill: Skill::Sword(SwordSkill::CripplingEviscerate),
1923                ability_id: "common.abilities.sword.crippling_eviscerate",
1924                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 855.0),
1925            },
1926            SkillIcon::Ability {
1927                skill: Skill::Sword(SwordSkill::CripplingBloodyGash),
1928                ability_id: "common.abilities.sword.crippling_bloody_gash",
1929                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 855.0),
1930            },
1931            SkillIcon::Ability {
1932                skill: Skill::Sword(SwordSkill::CleavingBladeFever),
1933                ability_id: "common.abilities.sword.cleaving_blade_fever",
1934                position: TopLeftWithMarginsOn(state.ids.sword_bg, 53.0, 2.0),
1935            },
1936            SkillIcon::Ability {
1937                skill: Skill::Sword(SwordSkill::CleavingSkySplitter),
1938                ability_id: "common.abilities.sword.cleaving_sky_splitter",
1939                position: TopLeftWithMarginsOn(state.ids.sword_bg, 142.0, 2.0),
1940            },
1941        ];
1942
1943        state.update(|s| {
1944            s.ids
1945                .skills
1946                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
1947        });
1948        state.update(|s| {
1949            s.ids
1950                .skill_lock_imgs
1951                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
1952        });
1953
1954        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
1955        events
1956    }
1957
1958    fn handle_axe_skills_window(
1959        &mut self,
1960        diary_tooltip: &Tooltip,
1961        state: &mut State<DiaryState>,
1962        ui: &mut UiCell,
1963        mut events: Vec<Event>,
1964    ) -> Vec<Event> {
1965        // Axe
1966        Image::new(self.imgs.axe_bg)
1967            .wh([924.0, 619.0])
1968            .mid_top_with_margin_on(state.ids.content_align, 65.0)
1969            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
1970            .set(state.ids.axe_bg, ui);
1971
1972        use PositionSpecifier::TopLeftWithMarginsOn;
1973        let skill_buttons = &[
1974            SkillIcon::Ability {
1975                skill: Skill::Axe(AxeSkill::BrutalSwing),
1976                ability_id: "common.abilities.axe.brutal_swing",
1977                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 424.0),
1978            },
1979            SkillIcon::Ability {
1980                skill: Skill::Axe(AxeSkill::Berserk),
1981                ability_id: "common.abilities.axe.berserk",
1982                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 374.0),
1983            },
1984            SkillIcon::Ability {
1985                skill: Skill::Axe(AxeSkill::RisingTide),
1986                ability_id: "common.abilities.axe.rising_tide",
1987                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 474.0),
1988            },
1989            SkillIcon::Ability {
1990                skill: Skill::Axe(AxeSkill::SavageSense),
1991                ability_id: "common.abilities.axe.savage_sense",
1992                position: TopLeftWithMarginsOn(state.ids.axe_bg, 187.0, 324.0),
1993            },
1994            SkillIcon::Ability {
1995                skill: Skill::Axe(AxeSkill::AdrenalineRush),
1996                ability_id: "common.abilities.axe.adrenaline_rush",
1997                position: TopLeftWithMarginsOn(state.ids.axe_bg, 187.0, 524.0),
1998            },
1999            SkillIcon::Ability {
2000                skill: Skill::Axe(AxeSkill::Execute),
2001                ability_id: "common.abilities.axe.execute",
2002                position: TopLeftWithMarginsOn(state.ids.axe_bg, 187.0, 424.0),
2003            },
2004            SkillIcon::Ability {
2005                skill: Skill::Axe(AxeSkill::Maelstrom),
2006                ability_id: "common.abilities.axe.maelstrom",
2007                position: TopLeftWithMarginsOn(state.ids.axe_bg, 4.0, 424.0),
2008            },
2009            SkillIcon::Ability {
2010                skill: Skill::Axe(AxeSkill::Rake),
2011                ability_id: "common.abilities.axe.rake",
2012                position: TopLeftWithMarginsOn(state.ids.axe_bg, 507.0, 325.0),
2013            },
2014            SkillIcon::Ability {
2015                skill: Skill::Axe(AxeSkill::Bloodfeast),
2016                ability_id: "common.abilities.axe.bloodfeast",
2017                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 74.0),
2018            },
2019            SkillIcon::Ability {
2020                skill: Skill::Axe(AxeSkill::FierceRaze),
2021                ability_id: "common.abilities.axe.fierce_raze",
2022                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 174.0),
2023            },
2024            SkillIcon::Ability {
2025                skill: Skill::Axe(AxeSkill::Furor),
2026                ability_id: "common.abilities.axe.furor",
2027                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 24.0),
2028            },
2029            SkillIcon::Ability {
2030                skill: Skill::Axe(AxeSkill::Fracture),
2031                ability_id: "common.abilities.axe.fracture",
2032                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 224.0),
2033            },
2034            SkillIcon::Ability {
2035                skill: Skill::Axe(AxeSkill::Lacerate),
2036                ability_id: "common.abilities.axe.lacerate",
2037                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 124.0),
2038            },
2039            SkillIcon::Ability {
2040                skill: Skill::Axe(AxeSkill::Riptide),
2041                ability_id: "common.abilities.axe.riptide",
2042                position: TopLeftWithMarginsOn(state.ids.axe_bg, 104.0, 124.0),
2043            },
2044            SkillIcon::Ability {
2045                skill: Skill::Axe(AxeSkill::SkullBash),
2046                ability_id: "common.abilities.axe.skull_bash",
2047                position: TopLeftWithMarginsOn(state.ids.axe_bg, 507.0, 523.0),
2048            },
2049            SkillIcon::Ability {
2050                skill: Skill::Axe(AxeSkill::Sunder),
2051                ability_id: "common.abilities.axe.sunder",
2052                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 674.0),
2053            },
2054            SkillIcon::Ability {
2055                skill: Skill::Axe(AxeSkill::Plunder),
2056                ability_id: "common.abilities.axe.plunder",
2057                position: TopLeftWithMarginsOn(state.ids.axe_bg, 387.0, 774.0),
2058            },
2059            SkillIcon::Ability {
2060                skill: Skill::Axe(AxeSkill::Defiance),
2061                ability_id: "common.abilities.axe.defiance",
2062                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 624.0),
2063            },
2064            SkillIcon::Ability {
2065                skill: Skill::Axe(AxeSkill::Keelhaul),
2066                ability_id: "common.abilities.axe.keelhaul",
2067                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 824.0),
2068            },
2069            SkillIcon::Ability {
2070                skill: Skill::Axe(AxeSkill::Bulkhead),
2071                ability_id: "common.abilities.axe.bulkhead",
2072                position: TopLeftWithMarginsOn(state.ids.axe_bg, 287.0, 724.0),
2073            },
2074            SkillIcon::Ability {
2075                skill: Skill::Axe(AxeSkill::Capsize),
2076                ability_id: "common.abilities.axe.capsize",
2077                position: TopLeftWithMarginsOn(state.ids.axe_bg, 104.0, 724.0),
2078            },
2079        ];
2080
2081        state.update(|s| {
2082            s.ids
2083                .skills
2084                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2085        });
2086        state.update(|s| {
2087            s.ids
2088                .skill_lock_imgs
2089                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2090        });
2091
2092        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2093        events
2094    }
2095
2096    fn handle_hammer_skills_window(
2097        &mut self,
2098        diary_tooltip: &Tooltip,
2099        state: &mut State<DiaryState>,
2100        ui: &mut UiCell,
2101        mut events: Vec<Event>,
2102    ) -> Vec<Event> {
2103        // Hammer
2104        Image::new(self.imgs.hammer_bg)
2105            .wh([924.0, 619.0])
2106            .mid_top_with_margin_on(state.ids.content_align, 65.0)
2107            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2108            .set(state.ids.hammer_bg, ui);
2109
2110        use PositionSpecifier::TopLeftWithMarginsOn;
2111        let skill_buttons = &[
2112            SkillIcon::Ability {
2113                skill: Skill::Hammer(HammerSkill::ScornfulSwipe),
2114                ability_id: "common.abilities.hammer.scornful_swipe",
2115                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 455.0, 424.0),
2116            },
2117            SkillIcon::Ability {
2118                skill: Skill::Hammer(HammerSkill::Tremor),
2119                ability_id: "common.abilities.hammer.tremor",
2120                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 172.0),
2121            },
2122            SkillIcon::Ability {
2123                skill: Skill::Hammer(HammerSkill::VigorousBash),
2124                ability_id: "common.abilities.hammer.vigorous_bash",
2125                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 272.0),
2126            },
2127            SkillIcon::Ability {
2128                skill: Skill::Hammer(HammerSkill::Retaliate),
2129                ability_id: "common.abilities.hammer.retaliate",
2130                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 122.0),
2131            },
2132            SkillIcon::Ability {
2133                skill: Skill::Hammer(HammerSkill::SpineCracker),
2134                ability_id: "common.abilities.hammer.spine_cracker",
2135                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 222.0),
2136            },
2137            SkillIcon::Ability {
2138                skill: Skill::Hammer(HammerSkill::Breach),
2139                ability_id: "common.abilities.hammer.breach",
2140                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 322.0),
2141            },
2142            SkillIcon::Ability {
2143                skill: Skill::Hammer(HammerSkill::IronTempest),
2144                ability_id: "common.abilities.hammer.iron_tempest",
2145                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 172.0),
2146            },
2147            SkillIcon::Ability {
2148                skill: Skill::Hammer(HammerSkill::Upheaval),
2149                ability_id: "common.abilities.hammer.upheaval",
2150                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 272.0),
2151            },
2152            SkillIcon::Ability {
2153                skill: Skill::Hammer(HammerSkill::Thunderclap),
2154                ability_id: "common.abilities.hammer.thunderclap",
2155                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 172.0),
2156            },
2157            SkillIcon::Ability {
2158                skill: Skill::Hammer(HammerSkill::SeismicShock),
2159                ability_id: "common.abilities.hammer.seismic_shock",
2160                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 272.0),
2161            },
2162            SkillIcon::Ability {
2163                skill: Skill::Hammer(HammerSkill::HeavyWhorl),
2164                ability_id: "common.abilities.hammer.heavy_whorl",
2165                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 576.0),
2166            },
2167            SkillIcon::Ability {
2168                skill: Skill::Hammer(HammerSkill::Intercept),
2169                ability_id: "common.abilities.hammer.intercept",
2170                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 398.0, 676.0),
2171            },
2172            SkillIcon::Ability {
2173                skill: Skill::Hammer(HammerSkill::PileDriver),
2174                ability_id: "common.abilities.hammer.pile_driver",
2175                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 526.0),
2176            },
2177            SkillIcon::Ability {
2178                skill: Skill::Hammer(HammerSkill::LungPummel),
2179                ability_id: "common.abilities.hammer.lung_pummel",
2180                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 626.0),
2181            },
2182            SkillIcon::Ability {
2183                skill: Skill::Hammer(HammerSkill::HelmCrusher),
2184                ability_id: "common.abilities.hammer.helm_crusher",
2185                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 284.0, 726.0),
2186            },
2187            SkillIcon::Ability {
2188                skill: Skill::Hammer(HammerSkill::Rampart),
2189                ability_id: "common.abilities.hammer.rampart",
2190                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 576.0),
2191            },
2192            SkillIcon::Ability {
2193                skill: Skill::Hammer(HammerSkill::Tenacity),
2194                ability_id: "common.abilities.hammer.tenacity",
2195                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 170.0, 676.0),
2196            },
2197            SkillIcon::Ability {
2198                skill: Skill::Hammer(HammerSkill::Earthshaker),
2199                ability_id: "common.abilities.hammer.earthshaker",
2200                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 576.0),
2201            },
2202            SkillIcon::Ability {
2203                skill: Skill::Hammer(HammerSkill::Judgement),
2204                ability_id: "common.abilities.hammer.judgement",
2205                position: TopLeftWithMarginsOn(state.ids.hammer_bg, 56.0, 676.0),
2206            },
2207        ];
2208
2209        state.update(|s| {
2210            s.ids
2211                .skills
2212                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2213        });
2214        state.update(|s| {
2215            s.ids
2216                .skill_lock_imgs
2217                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2218        });
2219
2220        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2221        events
2222    }
2223
2224    fn handle_sceptre_skills_window(
2225        &mut self,
2226        diary_tooltip: &Tooltip,
2227        state: &mut State<DiaryState>,
2228        ui: &mut UiCell,
2229        mut events: Vec<Event>,
2230    ) -> Vec<Event> {
2231        // Title text
2232        let tree_title = &self.localized_strings.get_msg("common-weapons-sceptre");
2233
2234        Text::new(tree_title)
2235            .mid_top_with_margin_on(state.ids.content_align, 2.0)
2236            .font_id(self.fonts.cyri.conrod_id)
2237            .font_size(self.fonts.cyri.scale(34))
2238            .color(TEXT_COLOR)
2239            .set(state.ids.tree_title_txt, ui);
2240
2241        // Number of skills per rectangle per weapon, start counting at 0
2242        // Maximum of 9 skills/8 indices
2243        let skills_top_l = 5;
2244        let skills_top_r = 5;
2245        let skills_bot_l = 5;
2246        let skills_bot_r = 0;
2247
2248        self.setup_state_for_skill_icons(
2249            state,
2250            ui,
2251            skills_top_l,
2252            skills_top_r,
2253            skills_bot_l,
2254            skills_bot_r,
2255        );
2256
2257        // Skill icons and buttons
2258        use skills::SceptreSkill::*;
2259        // Sceptre
2260        Image::new(animate_by_pulse(
2261            &self
2262                .item_imgs
2263                .img_ids_or_not_found_img(ItemKey::Simple("example_sceptre".to_string())),
2264            self.pulse,
2265        ))
2266        .wh(ART_SIZE)
2267        .middle_of(state.ids.content_align)
2268        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2269        .set(state.ids.sceptre_render, ui);
2270        use PositionSpecifier::MidTopWithMarginOn;
2271        let skill_buttons = &[
2272            // Top Left skills
2273            //        5 1 6
2274            //        3 0 4
2275            //        8 2 7
2276            SkillIcon::Descriptive {
2277                title: "hud-skill-sc_lifesteal_title",
2278                desc: "hud-skill-sc_lifesteal",
2279                image: self.imgs.skill_sceptre_lifesteal,
2280                position: MidTopWithMarginOn(state.ids.skills_top_l[0], 3.0),
2281                id: state.ids.skill_sceptre_lifesteal_0,
2282            },
2283            SkillIcon::Unlockable {
2284                skill: Skill::Sceptre(LDamage),
2285                image: self.imgs.magic_damage_skill,
2286                position: MidTopWithMarginOn(state.ids.skills_top_l[1], 3.0),
2287                id: state.ids.skill_sceptre_lifesteal_1,
2288            },
2289            SkillIcon::Unlockable {
2290                skill: Skill::Sceptre(LRange),
2291                image: self.imgs.magic_distance_skill,
2292                position: MidTopWithMarginOn(state.ids.skills_top_l[2], 3.0),
2293                id: state.ids.skill_sceptre_lifesteal_2,
2294            },
2295            SkillIcon::Unlockable {
2296                skill: Skill::Sceptre(LLifesteal),
2297                image: self.imgs.magic_lifesteal_skill,
2298                position: MidTopWithMarginOn(state.ids.skills_top_l[3], 3.0),
2299                id: state.ids.skill_sceptre_lifesteal_3,
2300            },
2301            SkillIcon::Unlockable {
2302                skill: Skill::Sceptre(LRegen),
2303                image: self.imgs.magic_energy_regen_skill,
2304                position: MidTopWithMarginOn(state.ids.skills_top_l[4], 3.0),
2305                id: state.ids.skill_sceptre_lifesteal_4,
2306            },
2307            // Top right skills
2308            SkillIcon::Descriptive {
2309                title: "hud-skill-sc_heal_title",
2310                desc: "hud-skill-sc_heal",
2311                image: self.imgs.skill_sceptre_heal,
2312                position: MidTopWithMarginOn(state.ids.skills_top_r[0], 3.0),
2313                id: state.ids.skill_sceptre_heal_0,
2314            },
2315            SkillIcon::Unlockable {
2316                skill: Skill::Sceptre(HHeal),
2317                image: self.imgs.heal_heal_skill,
2318                position: MidTopWithMarginOn(state.ids.skills_top_r[1], 3.0),
2319                id: state.ids.skill_sceptre_heal_1,
2320            },
2321            SkillIcon::Unlockable {
2322                skill: Skill::Sceptre(HDuration),
2323                image: self.imgs.heal_duration_skill,
2324                position: MidTopWithMarginOn(state.ids.skills_top_r[2], 3.0),
2325                id: state.ids.skill_sceptre_heal_2,
2326            },
2327            SkillIcon::Unlockable {
2328                skill: Skill::Sceptre(HRange),
2329                image: self.imgs.heal_radius_skill,
2330                position: MidTopWithMarginOn(state.ids.skills_top_r[3], 3.0),
2331                id: state.ids.skill_sceptre_heal_3,
2332            },
2333            SkillIcon::Unlockable {
2334                skill: Skill::Sceptre(HCost),
2335                image: self.imgs.heal_cost_skill,
2336                position: MidTopWithMarginOn(state.ids.skills_top_r[4], 3.0),
2337                id: state.ids.skill_sceptre_heal_4,
2338            },
2339            // Bottom left skills
2340            SkillIcon::Unlockable {
2341                skill: Skill::Sceptre(UnlockAura),
2342                image: self.imgs.skill_sceptre_aura,
2343                position: MidTopWithMarginOn(state.ids.skills_bot_l[0], 3.0),
2344                id: state.ids.skill_sceptre_aura_0,
2345            },
2346            SkillIcon::Unlockable {
2347                skill: Skill::Sceptre(AStrength),
2348                image: self.imgs.buff_damage_skill,
2349                position: MidTopWithMarginOn(state.ids.skills_bot_l[1], 3.0),
2350                id: state.ids.skill_sceptre_aura_1,
2351            },
2352            SkillIcon::Unlockable {
2353                skill: Skill::Sceptre(ADuration),
2354                image: self.imgs.buff_duration_skill,
2355                position: MidTopWithMarginOn(state.ids.skills_bot_l[2], 3.0),
2356                id: state.ids.skill_sceptre_aura_2,
2357            },
2358            SkillIcon::Unlockable {
2359                skill: Skill::Sceptre(ARange),
2360                image: self.imgs.buff_radius_skill,
2361                position: MidTopWithMarginOn(state.ids.skills_bot_l[3], 3.0),
2362                id: state.ids.skill_sceptre_aura_3,
2363            },
2364            SkillIcon::Unlockable {
2365                skill: Skill::Sceptre(ACost),
2366                image: self.imgs.buff_cost_skill,
2367                position: MidTopWithMarginOn(state.ids.skills_bot_l[4], 3.0),
2368                id: state.ids.skill_sceptre_aura_4,
2369            },
2370        ];
2371
2372        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2373        events
2374    }
2375
2376    fn handle_bow_skills_window(
2377        &mut self,
2378        diary_tooltip: &Tooltip,
2379        state: &mut State<DiaryState>,
2380        ui: &mut UiCell,
2381        mut events: Vec<Event>,
2382    ) -> Vec<Event> {
2383        Image::new(self.imgs.bow_bg)
2384            .wh([924.0, 619.0])
2385            .mid_top_with_margin_on(state.ids.content_align, 65.0)
2386            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2387            .set(state.ids.bow_bg, ui);
2388
2389        use PositionSpecifier::TopLeftWithMarginsOn;
2390        let skill_buttons = &[
2391            SkillIcon::Ability {
2392                skill: Skill::Bow(BowSkill::Foothold),
2393                ability_id: "common.abilities.bow.foothold",
2394                position: TopLeftWithMarginsOn(state.ids.bow_bg, 424.0, 368.0),
2395            },
2396            SkillIcon::Ability {
2397                skill: Skill::Bow(BowSkill::HeavyNock),
2398                ability_id: "common.abilities.bow.heavy_nock",
2399                position: TopLeftWithMarginsOn(state.ids.bow_bg, 424.0, 480.0),
2400            },
2401            SkillIcon::Ability {
2402                skill: Skill::Bow(BowSkill::ArdentHunt),
2403                ability_id: "common.abilities.bow.ardent_hunt",
2404                position: TopLeftWithMarginsOn(state.ids.bow_bg, 310.0, 204.0),
2405            },
2406            SkillIcon::Ability {
2407                skill: Skill::Bow(BowSkill::SepticShot),
2408                ability_id: "common.abilities.bow.septic_shot",
2409                position: TopLeftWithMarginsOn(state.ids.bow_bg, 310.0, 424.0),
2410            },
2411            SkillIcon::Ability {
2412                skill: Skill::Bow(BowSkill::Barrage),
2413                ability_id: "common.abilities.bow.barrage",
2414                position: TopLeftWithMarginsOn(state.ids.bow_bg, 310.0, 644.0),
2415            },
2416            SkillIcon::Ability {
2417                skill: Skill::Bow(BowSkill::StormChaser),
2418                ability_id: "common.abilities.bow.storm_chaser",
2419                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 154.0),
2420            },
2421            SkillIcon::Ability {
2422                skill: Skill::Bow(BowSkill::EagleEye),
2423                ability_id: "common.abilities.bow.eagle_eye",
2424                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 254.0),
2425            },
2426            SkillIcon::Ability {
2427                skill: Skill::Bow(BowSkill::IgniteArrow),
2428                ability_id: "common.abilities.bow.ignite_arrow",
2429                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 374.0),
2430            },
2431            SkillIcon::Ability {
2432                skill: Skill::Bow(BowSkill::DrenchArrow),
2433                ability_id: "common.abilities.bow.drench_arrow",
2434                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 474.0),
2435            },
2436            SkillIcon::Ability {
2437                skill: Skill::Bow(BowSkill::PiercingGale),
2438                ability_id: "common.abilities.bow.piercing_gale",
2439                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 594.0),
2440            },
2441            SkillIcon::Ability {
2442                skill: Skill::Bow(BowSkill::ThornStake),
2443                ability_id: "common.abilities.bow.thorn_stake",
2444                position: TopLeftWithMarginsOn(state.ids.bow_bg, 196.0, 694.0),
2445            },
2446            SkillIcon::Ability {
2447                skill: Skill::Bow(BowSkill::Heartseeker),
2448                ability_id: "common.abilities.bow.heartseeker",
2449                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 154.0),
2450            },
2451            SkillIcon::Ability {
2452                skill: Skill::Bow(BowSkill::Hawkstrike),
2453                ability_id: "common.abilities.bow.hawkstrike",
2454                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 254.0),
2455            },
2456            SkillIcon::Ability {
2457                skill: Skill::Bow(BowSkill::FreezeArrow),
2458                ability_id: "common.abilities.bow.freeze_arrow",
2459                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 374.0),
2460            },
2461            SkillIcon::Ability {
2462                skill: Skill::Bow(BowSkill::JoltArrow),
2463                ability_id: "common.abilities.bow.jolt_arrow",
2464                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 474.0),
2465            },
2466            SkillIcon::Ability {
2467                skill: Skill::Bow(BowSkill::Fusillade),
2468                ability_id: "common.abilities.bow.fusillade",
2469                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 594.0),
2470            },
2471            SkillIcon::Ability {
2472                skill: Skill::Bow(BowSkill::DeathVolley),
2473                ability_id: "common.abilities.bow.death_volley",
2474                position: TopLeftWithMarginsOn(state.ids.bow_bg, 82.0, 694.0),
2475            },
2476        ];
2477
2478        state.update(|s| {
2479            s.ids
2480                .skills
2481                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2482        });
2483        state.update(|s| {
2484            s.ids
2485                .skill_lock_imgs
2486                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2487        });
2488
2489        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2490        events
2491    }
2492
2493    fn handle_staff_skills_window(
2494        &mut self,
2495        diary_tooltip: &Tooltip,
2496        state: &mut State<DiaryState>,
2497        ui: &mut UiCell,
2498        mut events: Vec<Event>,
2499    ) -> Vec<Event> {
2500        use skills::StaffSkill::*;
2501
2502        Image::new(self.imgs.staff_bg)
2503            .wh([924.0, 619.0])
2504            .mid_top_with_margin_on(state.ids.content_align, 65.0)
2505            .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2506            .set(state.ids.staff_bg, ui);
2507
2508        use PositionSpecifier::TopLeftWithMarginsOn;
2509        let skill_buttons = &[
2510            SkillIcon::Ability {
2511                skill: Skill::Staff(FireShockwave),
2512                ability_id: "common.abilities.staff.fireshockwave",
2513                position: TopLeftWithMarginsOn(state.ids.staff_bg, 436.0, 421.0),
2514            },
2515            SkillIcon::Ability {
2516                skill: Skill::Staff(FireDash),
2517                ability_id: "common.abilities.staff.fire_dash",
2518                position: TopLeftWithMarginsOn(state.ids.staff_bg, 319.0, 331.0),
2519            },
2520            SkillIcon::Ability {
2521                skill: Skill::Staff(FlameCloak),
2522                ability_id: "common.abilities.staff.flame_cloak",
2523                position: TopLeftWithMarginsOn(state.ids.staff_bg, 319.0, 515.0),
2524            },
2525            SkillIcon::Ability {
2526                skill: Skill::Staff(FireBreath),
2527                ability_id: "common.abilities.staff.fire_breath",
2528                position: TopLeftWithMarginsOn(state.ids.staff_bg, 200.0, 515.0),
2529            },
2530            SkillIcon::Ability {
2531                skill: Skill::Staff(NapalmStrike),
2532                ability_id: "common.abilities.staff.napalm_strike",
2533                position: TopLeftWithMarginsOn(state.ids.staff_bg, 200.0, 331.0),
2534            },
2535            SkillIcon::Ability {
2536                skill: Skill::Staff(Pyroclasm),
2537                ability_id: "common.abilities.staff.pyroclasm",
2538                position: TopLeftWithMarginsOn(state.ids.staff_bg, 86.0, 422.0),
2539            },
2540        ];
2541
2542        state.update(|s| {
2543            s.ids
2544                .skills
2545                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2546        });
2547        state.update(|s| {
2548            s.ids
2549                .skill_lock_imgs
2550                .resize(skill_buttons.len(), &mut ui.widget_id_generator())
2551        });
2552
2553        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2554        events
2555    }
2556
2557    fn handle_mining_skills_window(
2558        &mut self,
2559        diary_tooltip: &Tooltip,
2560        state: &mut State<DiaryState>,
2561        ui: &mut UiCell,
2562        mut events: Vec<Event>,
2563    ) -> Vec<Event> {
2564        // Title text
2565        let tree_title = &self.localized_strings.get_msg("common-tool-mining");
2566
2567        Text::new(tree_title)
2568            .mid_top_with_margin_on(state.ids.content_align, 2.0)
2569            .font_id(self.fonts.cyri.conrod_id)
2570            .font_size(self.fonts.cyri.scale(34))
2571            .color(TEXT_COLOR)
2572            .set(state.ids.tree_title_txt, ui);
2573
2574        // Number of skills per rectangle per weapon, start counting at 0
2575        // Maximum of 9 skills/8 indices
2576        let skills_top_l = 4;
2577        let skills_top_r = 0;
2578        let skills_bot_l = 0;
2579        let skills_bot_r = 0;
2580
2581        self.setup_state_for_skill_icons(
2582            state,
2583            ui,
2584            skills_top_l,
2585            skills_top_r,
2586            skills_bot_l,
2587            skills_bot_r,
2588        );
2589
2590        // Skill icons and buttons
2591        use skills::MiningSkill::*;
2592        // Mining
2593        Image::new(animate_by_pulse(
2594            &self
2595                .item_imgs
2596                .img_ids_or_not_found_img(ItemKey::Simple("example_pick".to_string())),
2597            self.pulse,
2598        ))
2599        .wh(ART_SIZE)
2600        .middle_of(state.ids.content_align)
2601        .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
2602        .set(state.ids.pick_render, ui);
2603
2604        use PositionSpecifier::MidTopWithMarginOn;
2605        let skill_buttons = &[
2606            // Top Left skills
2607            //        5 1 6
2608            //        3 0 4
2609            //        8 2 7
2610            SkillIcon::Descriptive {
2611                title: "hud-skill-pick_strike_title",
2612                desc: "hud-skill-pick_strike",
2613                image: self.imgs.pickaxe,
2614                position: MidTopWithMarginOn(state.ids.skills_top_l[0], 3.0),
2615                id: state.ids.skill_pick_m1,
2616            },
2617            SkillIcon::Unlockable {
2618                skill: Skill::Pick(Speed),
2619                image: self.imgs.pickaxe_speed_skill,
2620                position: MidTopWithMarginOn(state.ids.skills_top_l[1], 3.0),
2621                id: state.ids.skill_pick_m1_0,
2622            },
2623            SkillIcon::Unlockable {
2624                skill: Skill::Pick(OreGain),
2625                image: self.imgs.pickaxe_oregain_skill,
2626                position: MidTopWithMarginOn(state.ids.skills_top_l[2], 3.0),
2627                id: state.ids.skill_pick_m1_1,
2628            },
2629            SkillIcon::Unlockable {
2630                skill: Skill::Pick(GemGain),
2631                image: self.imgs.pickaxe_gemgain_skill,
2632                position: MidTopWithMarginOn(state.ids.skills_top_l[3], 3.0),
2633                id: state.ids.skill_pick_m1_2,
2634            },
2635        ];
2636
2637        self.handle_skill_buttons(skill_buttons, ui, &mut events, diary_tooltip, state);
2638        events
2639    }
2640
2641    fn handle_skill_buttons(
2642        &mut self,
2643        icons: &[SkillIcon],
2644        ui: &mut UiCell,
2645        events: &mut Vec<Event>,
2646        diary_tooltip: &Tooltip,
2647        state: &mut State<DiaryState>,
2648    ) {
2649        for (i, icon) in icons.iter().enumerate() {
2650            match icon {
2651                SkillIcon::Descriptive {
2652                    title,
2653                    desc,
2654                    image,
2655                    position,
2656                    id,
2657                } => {
2658                    // TODO: shouldn't this be a `Image::new`?
2659                    Button::image(*image)
2660                        .w_h(74.0, 74.0)
2661                        .position(*position)
2662                        .with_tooltip(
2663                            self.tooltip_manager,
2664                            &self.localized_strings.get_msg(title),
2665                            &self.localized_strings.get_msg(desc),
2666                            diary_tooltip,
2667                            TEXT_COLOR,
2668                        )
2669                        .set(*id, ui);
2670                },
2671                SkillIcon::Unlockable {
2672                    skill,
2673                    image,
2674                    position,
2675                    id,
2676                } => self.create_unlock_skill_button(
2677                    *skill,
2678                    *image,
2679                    *position,
2680                    *id,
2681                    ui,
2682                    events,
2683                    diary_tooltip,
2684                ),
2685                SkillIcon::Ability {
2686                    skill,
2687                    ability_id,
2688                    position,
2689                } => self.create_unlock_ability_button(
2690                    *skill,
2691                    ability_id,
2692                    *position,
2693                    i,
2694                    ui,
2695                    events,
2696                    diary_tooltip,
2697                    state,
2698                ),
2699            }
2700        }
2701    }
2702
2703    fn setup_state_for_skill_icons(
2704        &mut self,
2705        state: &mut State<DiaryState>,
2706        ui: &mut UiCell,
2707        skills_top_l: usize,
2708        skills_top_r: usize,
2709        skills_bot_l: usize,
2710        skills_bot_r: usize,
2711    ) {
2712        // Update widget id array len
2713        state.update(|s| {
2714            s.ids
2715                .skills_top_l
2716                .resize(skills_top_l, &mut ui.widget_id_generator())
2717        });
2718        state.update(|s| {
2719            s.ids
2720                .skills_top_r
2721                .resize(skills_top_r, &mut ui.widget_id_generator())
2722        });
2723        state.update(|s| {
2724            s.ids
2725                .skills_bot_l
2726                .resize(skills_bot_l, &mut ui.widget_id_generator())
2727        });
2728        state.update(|s| {
2729            s.ids
2730                .skills_bot_r
2731                .resize(skills_bot_r, &mut ui.widget_id_generator())
2732        });
2733
2734        // Create Background Images to place skill icons on them later
2735        // Create central skill first, others around it:
2736        //
2737        //        5 1 6
2738        //        3 0 4
2739        //        8 2 7
2740        //
2741        //
2742        let offset_0 = 22.0;
2743        let offset_1 = -122.0;
2744        let offset_2 = offset_1 - -20.0;
2745
2746        let skill_pos = |idx, align, central_skill| {
2747            use PositionSpecifier::*;
2748            match idx {
2749                // Central skill
2750                0 => MiddleOf(align),
2751                // 12:00
2752                1 => UpFrom(central_skill, offset_0),
2753                // 6:00
2754                2 => DownFrom(central_skill, offset_0),
2755                // 3:00
2756                3 => LeftFrom(central_skill, offset_0),
2757                // 9:00
2758                4 => RightFrom(central_skill, offset_0),
2759                // 10:30
2760                5 => TopLeftWithMarginsOn(central_skill, offset_1, offset_2),
2761                // 1:30
2762                6 => TopRightWithMarginsOn(central_skill, offset_1, offset_2),
2763                // 4:30
2764                7 => BottomLeftWithMarginsOn(central_skill, offset_1, offset_2),
2765                // 7:30
2766                8 => BottomRightWithMarginsOn(central_skill, offset_1, offset_2),
2767                buttons => {
2768                    panic!("{} > 8 position number", buttons);
2769                },
2770            }
2771        };
2772
2773        // TOP-LEFT Skills
2774        //
2775        // TODO: Why this uses while loop on field of struct and not just
2776        // `for i in 0..skils_top_l`?
2777        while self.created_btns_top_l < skills_top_l {
2778            let pos = skill_pos(
2779                self.created_btns_top_l,
2780                state.ids.skills_top_l_align,
2781                state.ids.skills_top_l[0],
2782            );
2783            Button::image(self.imgs.wpn_icon_border_skills)
2784                .w_h(80.0, 100.0)
2785                .position(pos)
2786                .set(state.ids.skills_top_l[self.created_btns_top_l], ui);
2787            self.created_btns_top_l += 1;
2788        }
2789        // TOP-RIGHT Skills
2790        while self.created_btns_top_r < skills_top_r {
2791            let pos = skill_pos(
2792                self.created_btns_top_r,
2793                state.ids.skills_top_r_align,
2794                state.ids.skills_top_r[0],
2795            );
2796            Button::image(self.imgs.wpn_icon_border_skills)
2797                .w_h(80.0, 100.0)
2798                .position(pos)
2799                .set(state.ids.skills_top_r[self.created_btns_top_r], ui);
2800            self.created_btns_top_r += 1;
2801        }
2802        // BOTTOM-LEFT Skills
2803        while self.created_btns_bot_l < skills_bot_l {
2804            let pos = skill_pos(
2805                self.created_btns_bot_l,
2806                state.ids.skills_bot_l_align,
2807                state.ids.skills_bot_l[0],
2808            );
2809            Button::image(self.imgs.wpn_icon_border_skills)
2810                .w_h(80.0, 100.0)
2811                .position(pos)
2812                .set(state.ids.skills_bot_l[self.created_btns_bot_l], ui);
2813            self.created_btns_bot_l += 1;
2814        }
2815        // BOTTOM-RIGHT Skills
2816        while self.created_btns_bot_r < skills_bot_r {
2817            let pos = skill_pos(
2818                self.created_btns_bot_r,
2819                state.ids.skills_bot_r_align,
2820                state.ids.skills_bot_r[0],
2821            );
2822            Button::image(self.imgs.wpn_icon_border_skills)
2823                .w_h(80.0, 100.0)
2824                .position(pos)
2825                .set(state.ids.skills_bot_r[self.created_btns_bot_r], ui);
2826            self.created_btns_bot_r += 1;
2827        }
2828    }
2829
2830    fn create_unlock_skill_button(
2831        &mut self,
2832        skill: Skill,
2833        skill_image: image::Id,
2834        position: PositionSpecifier,
2835        widget_id: widget::Id,
2836        ui: &mut UiCell,
2837        events: &mut Vec<Event>,
2838        diary_tooltip: &Tooltip,
2839    ) {
2840        let label = if self.skill_set.prerequisites_met(skill) {
2841            let current = self.skill_set.skill_level(skill).unwrap_or(0);
2842            let max = skill.max_level();
2843            format!("{}/{}", current, max)
2844        } else {
2845            "".to_owned()
2846        };
2847
2848        let label_color = if self.skill_set.is_at_max_level(skill) {
2849            TEXT_COLOR
2850        } else if self.skill_set.sufficient_skill_points(skill) {
2851            HP_COLOR
2852        } else {
2853            CRITICAL_HP_COLOR
2854        };
2855
2856        let image_color = if self.skill_set.prerequisites_met(skill) {
2857            TEXT_COLOR
2858        } else {
2859            Color::Rgba(0.41, 0.41, 0.41, 0.7)
2860        };
2861
2862        let skill_strings = skill_strings(skill);
2863        let (title, description) =
2864            skill_strings.localize(self.localized_strings, self.skill_set, skill);
2865
2866        let button = Button::image(skill_image)
2867            .w_h(74.0, 74.0)
2868            .position(position)
2869            .label(&label)
2870            .label_y(conrod_core::position::Relative::Scalar(-47.0))
2871            .label_x(conrod_core::position::Relative::Scalar(0.0))
2872            .label_color(label_color)
2873            .label_font_size(self.fonts.cyri.scale(15))
2874            .label_font_id(self.fonts.cyri.conrod_id)
2875            .image_color(image_color)
2876            .with_tooltip(
2877                self.tooltip_manager,
2878                &title,
2879                &description,
2880                diary_tooltip,
2881                TEXT_COLOR,
2882            )
2883            .set(widget_id, ui);
2884
2885        if button.was_clicked() {
2886            events.push(Event::UnlockSkill(skill));
2887        };
2888    }
2889
2890    fn create_unlock_ability_button(
2891        &mut self,
2892        skill: Skill,
2893        ability_id: &str,
2894        position: PositionSpecifier,
2895        widget_index: usize,
2896        ui: &mut UiCell,
2897        events: &mut Vec<Event>,
2898        diary_tooltip: &Tooltip,
2899        state: &mut State<DiaryState>,
2900    ) {
2901        let locked = !self.skill_set.prerequisites_met(skill);
2902        let owned = self.skill_set.has_skill(skill);
2903        let image_color = if owned {
2904            TEXT_COLOR
2905        } else {
2906            Color::Rgba(0.41, 0.41, 0.41, 0.7)
2907        };
2908
2909        let (title, description) = util::ability_description(ability_id, self.localized_strings);
2910
2911        let sp_cost = sp(self.localized_strings, self.skill_set, skill);
2912
2913        let description = format!("{description}\n{sp_cost}");
2914
2915        let button = Button::image(util::ability_image(self.imgs, ability_id))
2916            .w_h(76.0, 76.0)
2917            .position(position)
2918            .image_color(image_color)
2919            .with_tooltip(
2920                self.tooltip_manager,
2921                &title,
2922                &description,
2923                diary_tooltip,
2924                TEXT_COLOR,
2925            )
2926            .set(state.ids.skills[widget_index], ui);
2927
2928        // Lock Image
2929        if locked {
2930            Image::new(self.imgs.lock)
2931                .w_h(76.0, 76.0)
2932                .middle_of(state.ids.skills[widget_index])
2933                .graphics_for(state.ids.skills[widget_index])
2934                .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.8)))
2935                .set(state.ids.skill_lock_imgs[widget_index], ui);
2936        }
2937
2938        if button.was_clicked() {
2939            events.push(Event::UnlockSkill(skill));
2940        };
2941    }
2942}
2943
2944/// Returns skill info as a tuple of title and description.
2945///
2946/// If you want to get localized version, use `SkillStrings::localize` method
2947fn skill_strings(skill: Skill) -> SkillStrings<'static> {
2948    match skill {
2949        // general tree
2950        Skill::UnlockGroup(s) => unlock_skill_strings(s),
2951        // weapon trees
2952        Skill::Sceptre(s) => sceptre_skill_strings(s),
2953        // movement trees
2954        Skill::Climb(s) => climb_skill_strings(s),
2955        Skill::Swim(s) => swim_skill_strings(s),
2956        // mining
2957        Skill::Pick(s) => mining_skill_strings(s),
2958        _ => SkillStrings::plain("", ""),
2959    }
2960}
2961
2962fn unlock_skill_strings(group: SkillGroupKind) -> SkillStrings<'static> {
2963    match group {
2964        SkillGroupKind::Weapon(ToolKind::Sword) => {
2965            SkillStrings::plain("hud-skill-unlck_sword_title", "hud-skill-unlck_sword")
2966        },
2967        SkillGroupKind::Weapon(ToolKind::Axe) => {
2968            SkillStrings::plain("hud-skill-unlck_axe_title", "hud-skill-unlck_axe")
2969        },
2970        SkillGroupKind::Weapon(ToolKind::Hammer) => {
2971            SkillStrings::plain("hud-skill-unlck_hammer_title", "hud-skill-unlck_hammer")
2972        },
2973        SkillGroupKind::Weapon(ToolKind::Bow) => {
2974            SkillStrings::plain("hud-skill-unlck_bow_title", "hud-skill-unlck_bow")
2975        },
2976        SkillGroupKind::Weapon(ToolKind::Staff) => {
2977            SkillStrings::plain("hud-skill-unlck_staff_title", "hud-skill-unlck_staff")
2978        },
2979        SkillGroupKind::Weapon(ToolKind::Sceptre) => {
2980            SkillStrings::plain("hud-skill-unlck_sceptre_title", "hud-skill-unlck_sceptre")
2981        },
2982        SkillGroupKind::General
2983        | SkillGroupKind::Weapon(
2984            ToolKind::Dagger
2985            | ToolKind::Shield
2986            | ToolKind::Spear
2987            | ToolKind::Blowgun
2988            | ToolKind::Debug
2989            | ToolKind::Farming
2990            | ToolKind::Instrument
2991            | ToolKind::Throwable
2992            | ToolKind::Pick
2993            | ToolKind::Shovel
2994            | ToolKind::Natural
2995            | ToolKind::Empty,
2996        ) => {
2997            tracing::warn!("Requesting title for unlocking unexpected skill group");
2998            SkillStrings::Empty
2999        },
3000    }
3001}
3002
3003fn sceptre_skill_strings(skill: SceptreSkill) -> SkillStrings<'static> {
3004    let modifiers = SKILL_MODIFIERS.sceptre_tree;
3005    match skill {
3006        // Lifesteal beam upgrades
3007        SceptreSkill::LDamage => SkillStrings::with_mult(
3008            "hud-skill-sc_lifesteal_damage_title",
3009            "hud-skill-sc_lifesteal_damage",
3010            modifiers.beam.damage,
3011        ),
3012        SceptreSkill::LRange => SkillStrings::with_mult(
3013            "hud-skill-sc_lifesteal_range_title",
3014            "hud-skill-sc_lifesteal_range",
3015            modifiers.beam.range,
3016        ),
3017        SceptreSkill::LLifesteal => SkillStrings::with_mult(
3018            "hud-skill-sc_lifesteal_lifesteal_title",
3019            "hud-skill-sc_lifesteal_lifesteal",
3020            modifiers.beam.lifesteal,
3021        ),
3022        SceptreSkill::LRegen => SkillStrings::with_mult(
3023            "hud-skill-sc_lifesteal_regen_title",
3024            "hud-skill-sc_lifesteal_regen",
3025            modifiers.beam.energy_regen,
3026        ),
3027        // Healing aura upgrades
3028        SceptreSkill::HHeal => SkillStrings::with_mult(
3029            "hud-skill-sc_heal_heal_title",
3030            "hud-skill-sc_heal_heal",
3031            modifiers.healing_aura.strength,
3032        ),
3033        SceptreSkill::HRange => SkillStrings::with_mult(
3034            "hud-skill-sc_heal_range_title",
3035            "hud-skill-sc_heal_range",
3036            modifiers.healing_aura.range,
3037        ),
3038        SceptreSkill::HDuration => SkillStrings::with_mult(
3039            "hud-skill-sc_heal_duration_title",
3040            "hud-skill-sc_heal_duration",
3041            modifiers.healing_aura.duration,
3042        ),
3043        SceptreSkill::HCost => SkillStrings::with_mult(
3044            "hud-skill-sc_heal_cost_title",
3045            "hud-skill-sc_heal_cost",
3046            modifiers.healing_aura.energy_cost,
3047        ),
3048        // Warding aura upgrades
3049        SceptreSkill::UnlockAura => SkillStrings::plain(
3050            "hud-skill-sc_wardaura_unlock_title",
3051            "hud-skill-sc_wardaura_unlock",
3052        ),
3053        SceptreSkill::AStrength => SkillStrings::with_mult(
3054            "hud-skill-sc_wardaura_strength_title",
3055            "hud-skill-sc_wardaura_strength",
3056            modifiers.warding_aura.strength,
3057        ),
3058        SceptreSkill::ADuration => SkillStrings::with_mult(
3059            "hud-skill-sc_wardaura_duration_title",
3060            "hud-skill-sc_wardaura_duration",
3061            modifiers.warding_aura.duration,
3062        ),
3063        SceptreSkill::ARange => SkillStrings::with_mult(
3064            "hud-skill-sc_wardaura_range_title",
3065            "hud-skill-sc_wardaura_range",
3066            modifiers.warding_aura.range,
3067        ),
3068        SceptreSkill::ACost => SkillStrings::with_mult(
3069            "hud-skill-sc_wardaura_cost_title",
3070            "hud-skill-sc_wardaura_cost",
3071            modifiers.warding_aura.energy_cost,
3072        ),
3073    }
3074}
3075
3076fn climb_skill_strings(skill: ClimbSkill) -> SkillStrings<'static> {
3077    let modifiers = SKILL_MODIFIERS.general_tree.climb;
3078    match skill {
3079        ClimbSkill::Cost => SkillStrings::with_mult(
3080            "hud-skill-climbing_cost_title",
3081            "hud-skill-climbing_cost",
3082            modifiers.energy_cost,
3083        ),
3084        ClimbSkill::Speed => SkillStrings::with_mult(
3085            "hud-skill-climbing_speed_title",
3086            "hud-skill-climbing_speed",
3087            modifiers.speed,
3088        ),
3089    }
3090}
3091
3092fn swim_skill_strings(skill: SwimSkill) -> SkillStrings<'static> {
3093    let modifiers = SKILL_MODIFIERS.general_tree.swim;
3094    match skill {
3095        SwimSkill::Speed => SkillStrings::with_mult(
3096            "hud-skill-swim_speed_title",
3097            "hud-skill-swim_speed",
3098            modifiers.speed,
3099        ),
3100    }
3101}
3102
3103fn mining_skill_strings(skill: MiningSkill) -> SkillStrings<'static> {
3104    let modifiers = SKILL_MODIFIERS.mining_tree;
3105    match skill {
3106        MiningSkill::Speed => SkillStrings::with_mult(
3107            "hud-skill-pick_strike_speed_title",
3108            "hud-skill-pick_strike_speed",
3109            modifiers.speed,
3110        ),
3111        MiningSkill::OreGain => SkillStrings::with_const(
3112            "hud-skill-pick_strike_oregain_title",
3113            "hud-skill-pick_strike_oregain",
3114            (modifiers.ore_gain * 100.0).round() as u32,
3115        ),
3116        MiningSkill::GemGain => SkillStrings::with_const(
3117            "hud-skill-pick_strike_gemgain_title",
3118            "hud-skill-pick_strike_gemgain",
3119            (modifiers.gem_gain * 100.0).round() as u32,
3120        ),
3121    }
3122}
3123
3124/// Helper object used returned by `skill_strings` as source for
3125/// later internationalization and formatting.
3126enum SkillStrings<'a> {
3127    Plain {
3128        title: &'a str,
3129        desc: &'a str,
3130    },
3131    WithConst {
3132        title: &'a str,
3133        desc: &'a str,
3134        constant: u32,
3135    },
3136    WithMult {
3137        title: &'a str,
3138        desc: &'a str,
3139        multiplier: f32,
3140    },
3141    Empty,
3142}
3143
3144impl<'a> SkillStrings<'a> {
3145    fn plain(title: &'a str, desc: &'a str) -> Self { Self::Plain { title, desc } }
3146
3147    fn with_const(title: &'a str, desc: &'a str, constant: u32) -> Self {
3148        Self::WithConst {
3149            title,
3150            desc,
3151            constant,
3152        }
3153    }
3154
3155    fn with_mult(title: &'a str, desc: &'a str, multiplier: f32) -> Self {
3156        Self::WithMult {
3157            title,
3158            desc,
3159            multiplier,
3160        }
3161    }
3162
3163    fn localize<'loc>(
3164        &self,
3165        i18n: &'loc Localization,
3166        skill_set: &SkillSet,
3167        skill: Skill,
3168    ) -> (Cow<'loc, str>, Cow<'loc, str>) {
3169        match self {
3170            Self::Plain { title, desc } => {
3171                let title = i18n.get_msg(title);
3172
3173                let args = i18n::fluent_args! {
3174                    "SP" => sp(i18n, skill_set, skill),
3175                };
3176                let desc = i18n.get_msg_ctx(desc, &args);
3177
3178                (title, desc)
3179            },
3180            Self::WithConst {
3181                title,
3182                desc,
3183                constant,
3184            } => {
3185                let title = i18n.get_msg(title);
3186                let args = i18n::fluent_args! {
3187                    "boost" => constant,
3188                    "SP" => sp(i18n, skill_set, skill),
3189                };
3190                let desc = i18n.get_msg_ctx(desc, &args);
3191
3192                (title, desc)
3193            },
3194            Self::WithMult {
3195                title,
3196                desc,
3197                multiplier,
3198            } => {
3199                let percentage = hud::multiplier_to_percentage(*multiplier).abs();
3200
3201                let title = i18n.get_msg(title);
3202
3203                let args = i18n::fluent_args! {
3204                    "boost" => format!("{percentage:.0}"),
3205                    "SP" => sp(i18n, skill_set, skill),
3206                };
3207                let desc = i18n.get_msg_ctx(desc, &args);
3208
3209                (title, desc)
3210            },
3211            Self::Empty => (Cow::Borrowed(""), Cow::Borrowed("")),
3212        }
3213    }
3214}
3215
3216/// The number of variants of the [`CharacterStat`] enum.
3217const STAT_COUNT: usize = 15;
3218
3219#[derive(EnumIter)]
3220enum CharacterStat {
3221    Name,
3222    BattleMode,
3223    Waypoint,
3224    Hitpoints,
3225    Energy,
3226    Poise,
3227    CombatRating,
3228    Protection,
3229    StunResistance,
3230    PrecisionPower,
3231    EnergyReward,
3232    Stealth,
3233    WeaponPower,
3234    WeaponSpeed,
3235    WeaponEffectPower,
3236}
3237
3238impl CharacterStat {
3239    fn localized_str<'a>(&self, i18n: &'a Localization) -> Cow<'a, str> {
3240        use CharacterStat::*;
3241
3242        match self {
3243            Name => i18n.get_msg("character_window-character_name"),
3244            BattleMode => i18n.get_msg("hud-battle-mode"),
3245            Waypoint => i18n.get_msg("hud-waypoint"),
3246            Hitpoints => i18n.get_msg("hud-bag-health"),
3247            Energy => i18n.get_msg("hud-bag-energy"),
3248            CombatRating => i18n.get_msg("hud-bag-combat_rating"),
3249            Protection => i18n.get_msg("hud-bag-protection"),
3250            StunResistance => i18n.get_msg("hud-bag-stun_res"),
3251            Poise => i18n.get_msg("common-stats-poise_res"),
3252            PrecisionPower => i18n.get_msg("common-stats-precision_power"),
3253            EnergyReward => i18n.get_msg("common-stats-energy_reward"),
3254            Stealth => i18n.get_msg("common-stats-stealth"),
3255            WeaponPower => i18n.get_msg("common-stats-power"),
3256            WeaponSpeed => i18n.get_msg("common-stats-speed"),
3257            WeaponEffectPower => i18n.get_msg("common-stats-effect-power"),
3258        }
3259    }
3260}
3261
3262fn sp<'loc>(i18n: &'loc Localization, skill_set: &SkillSet, skill: Skill) -> Cow<'loc, str> {
3263    let current_level = skill_set.skill_level(skill);
3264    if matches!(current_level, Ok(level) if level == skill.max_level()) {
3265        Cow::Borrowed("")
3266    } else {
3267        i18n.get_msg_ctx("hud-skill-req_sp", &i18n::fluent_args! {
3268            "number" => skill_set.skill_cost(skill),
3269        })
3270    }
3271}