1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#![warn(clippy::pedantic)]
//#![warn(clippy::nursery)]
use crate::comp::skillset::{skills::Skill, SkillGroupKind, SkillSet};

use crate::assets::{self, AssetExt};
use serde::{Deserialize, Serialize};
use tracing::warn;

/// `SkillSetBuilder` preset. Consider using loading from assets, when possible.
/// When you're adding new enum variant,
/// handle it in [`with_preset`](SkillSetBuilder::with_preset) method
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Preset {
    Rank1,
    Rank2,
    Rank3,
    Rank4,
    Rank5,
}

#[derive(Debug, Deserialize, Clone)]
struct SkillSetTree(Vec<SkillNode>);
impl assets::Asset for SkillSetTree {
    type Loader = assets::RonLoader;

    const EXTENSION: &'static str = "ron";
}

#[derive(Debug, Deserialize, Clone)]
enum SkillNode {
    Tree(String),
    Skill((Skill, u16)),
    Group(SkillGroupKind),
}

#[must_use]
fn skills_from_asset_expect(asset_specifier: &str) -> Vec<(Skill, u16)> {
    let nodes = SkillSetTree::load_expect(asset_specifier).read();

    skills_from_nodes(&nodes.0)
}

#[must_use]
fn skills_from_nodes(nodes: &[SkillNode]) -> Vec<(Skill, u16)> {
    let mut skills = Vec::new();
    for node in nodes {
        match node {
            SkillNode::Tree(asset) => {
                skills.append(&mut skills_from_asset_expect(asset));
            },
            SkillNode::Skill(req) => {
                skills.push(*req);
            },
            SkillNode::Group(group) => {
                skills.push((Skill::UnlockGroup(*group), 1));
            },
        }
    }

    skills
}

#[derive(Default)]
pub struct SkillSetBuilder(SkillSet);

impl SkillSetBuilder {
    /// Creates `SkillSetBuilder` from `asset_specifier`
    #[must_use]
    pub fn from_asset_expect(asset_specifier: &str) -> Self {
        let builder = Self::default();

        builder.with_asset_expect(asset_specifier)
    }

    /// Applies `asset_specifier` with needed skill tree
    #[must_use]
    pub fn with_asset_expect(mut self, asset_specifier: &str) -> Self {
        let tree = skills_from_asset_expect(asset_specifier);
        for (skill, level) in tree {
            self = self.with_skill(skill, level);
        }

        self
    }

    /// Creates `SkillSetBuilder` for given preset
    #[must_use]
    pub fn from_preset(preset: Preset) -> Self {
        let builder = Self::default();

        builder.with_preset(preset)
    }

    /// Applies preset
    #[must_use]
    pub fn with_preset(self, preset: Preset) -> Self {
        match preset {
            Preset::Rank1 => self.with_asset_expect("common.skillset.preset.rank1.fullskill"),
            Preset::Rank2 => self.with_asset_expect("common.skillset.preset.rank2.fullskill"),
            Preset::Rank3 => self.with_asset_expect("common.skillset.preset.rank3.fullskill"),
            Preset::Rank4 => self.with_asset_expect("common.skillset.preset.rank4.fullskill"),
            Preset::Rank5 => self.with_asset_expect("common.skillset.preset.rank5.fullskill"),
        }
    }

    #[must_use]
    /// # Panics
    /// will panic only in tests
    /// 1) If added skill doesn't have any group
    /// 2) If added skill already applied
    /// 3) If added skill wasn't applied at the end
    pub fn with_skill(mut self, skill: Skill, level: u16) -> Self {
        let Some(group) = skill.skill_group_kind() else {
            let err = format!(
                "Tried to add skill: {skill:?} which does not have an associated skill group."
            );
            common_base::dev_panic!(err, or return self);
        };

        let SkillSetBuilder(ref mut skill_set) = self;
        if skill_is_applied(skill_set, skill, level) {
            let err = format!(
                "Tried to add skill: {skill:?} with level {level:?} which is already applied"
            );
            common_base::dev_panic!(err, or return self);
        }
        for _ in 0..level {
            skill_set.add_skill_points(group, skill_set.skill_cost(skill));
            if let Err(err) = skill_set.unlock_skill(skill) {
                let err_msg = format!("Failed to add skill: {skill:?}. Error: {err:?}");
                common_base::dev_panic!(err_msg);
            }
        }
        if !skill_is_applied(skill_set, skill, level) {
            let err = format!(
                "Failed to add skill: {skill:?}. Verify that it has the appropriate skill group \
                 available and meets all prerequisite skills."
            );
            common_base::dev_panic!(err);
        }
        self
    }

    #[must_use]
    pub fn build(self) -> SkillSet { self.0 }
}

#[must_use]
fn skill_is_applied(skill_set: &SkillSet, skill: Skill, level: u16) -> bool {
    if let Ok(applied_level) = skill_set.skill_level(skill) {
        applied_level == level
    } else {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_all_skillset_assets() {
        let skillsets =
            assets::load_rec_dir::<SkillSetTree>("common.skillset").expect("load skillsets");
        for skillset in skillsets.read().ids() {
            let skillset = SkillSetTree::load_expect(skillset).read();

            drop({
                let mut skillset_builder = SkillSetBuilder::default();
                let nodes = &*skillset.0;
                let tree = skills_from_nodes(nodes);
                for (skill, level) in tree {
                    skillset_builder = skillset_builder.with_skill(skill, level);
                }

                skillset_builder
            });
        }
    }
}