essences/
pets.rs

1use crate::items::AttributeId;
2use crate::prelude::*;
3
4use std::collections::BTreeMap;
5
6#[declare]
7pub type PetId = Uuid;
8
9#[declare]
10pub type PetSlotId = usize;
11
12#[declare]
13pub type PetRarityId = Uuid;
14
15#[derive(
16    Default,
17    PartialEq,
18    Eq,
19    Hash,
20    Debug,
21    Clone,
22    Serialize,
23    Deserialize,
24    Tsify,
25    JsonSchema,
26    i18n::Translate,
27)]
28pub struct PetRarity {
29    #[schemars(schema_with = "id_schema")]
30    pub id: PetRarityId,
31
32    #[schemars(title = "Название редкости")]
33    #[translate]
34    pub name: i18n::I18nString,
35
36    #[schemars(title = "Сортировка")]
37    pub order: u64,
38
39    #[schemars(title = "Цвет редкости", schema_with = "color_schema")]
40    pub color: String,
41
42    #[schemars(title = "Цвет заднего фона редкости", schema_with = "color_schema")]
43    pub bg_color: String,
44
45    #[schemars(title = "Рамка", schema_with = "asset_pet_rarity_icon_schema")]
46    pub icon_path: String,
47
48    #[schemars(
49        title = "Квадратная рамка",
50        schema_with = "asset_pet_rarity_square_icon_schema"
51    )]
52    pub square_icon_path: String,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
56pub struct PetSecondaryStat {
57    #[schemars(title = "Id атрибута", schema_with = "attribute_link_id_schema")]
58    pub attribute_id: AttributeId,
59    #[schemars(title = "Базовое значение")]
60    pub base_value: i64,
61    #[schemars(title = "Значение за уровень")]
62    pub per_level_value: i64,
63}
64
65#[derive(
66    Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema, Default, i18n::Translate,
67)]
68#[tsify(from_wasm_abi)]
69pub struct PetTemplate {
70    #[schemars(schema_with = "id_schema")]
71    pub id: PetId,
72
73    #[schemars(title = "Имя пета")]
74    #[translate]
75    pub name: i18n::I18nString,
76
77    #[schemars(title = "Иконка", schema_with = "asset_pet_icon_schema")]
78    pub icon_path: String,
79
80    #[schemars(title = "Спайн пета", schema_with = "asset_unit_spine_skin")]
81    pub spine_path: String,
82
83    #[schemars(title = "Id редкости", schema_with = "pet_rarity_link_id_schema")]
84    pub rarity_id: PetRarityId,
85
86    #[schemars(title = "Статы пета")]
87    pub stats: Vec<PetSecondaryStat>,
88
89    #[schemars(title = "Показывается в окне гачи, умеет выпадать из гачи")]
90    pub is_gacha_pet: bool,
91
92    /// Real half of the pet's fixed facet pair. Part of the pet's identity: it
93    /// is not rolled on drop and cannot be re-socketed.
94    #[schemars(title = "Грань мира Real")]
95    pub real_facet: crate::pet_facets::PetFacet,
96
97    /// Fantasy half of the same pair.
98    #[schemars(title = "Грань мира Fantasy")]
99    pub fantasy_facet: crate::pet_facets::PetFacet,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
103pub struct PetComputedSecondaryStat {
104    pub attribute_id: AttributeId,
105    pub value: i64,
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
109#[tsify(from_wasm_abi)]
110pub struct Pet {
111    pub template_id: PetId,
112    pub name: i18n::I18nString,
113    pub icon_path: String,
114    pub rarity: PetRarity,
115    pub level: i64,
116    pub shards_amount: i64,
117    pub stats: Vec<PetComputedSecondaryStat>,
118}
119
120impl Pet {
121    pub fn from_template(
122        template: &PetTemplate,
123        rarity: PetRarity,
124        level: i64,
125        shards_amount: i64,
126    ) -> Self {
127        let stats = template
128            .stats
129            .iter()
130            .map(|s| PetComputedSecondaryStat {
131                attribute_id: s.attribute_id,
132                value: s.base_value + s.per_level_value * (level - 1),
133            })
134            .collect();
135
136        Pet {
137            template_id: template.id,
138            name: template.name.clone(),
139            icon_path: template.icon_path.clone(),
140            rarity,
141            level,
142            shards_amount,
143            stats,
144        }
145    }
146
147    pub fn recompute_stats(&mut self, template: &PetTemplate) {
148        self.stats = template
149            .stats
150            .iter()
151            .map(|s| PetComputedSecondaryStat {
152                attribute_id: s.attribute_id,
153                value: s.base_value + s.per_level_value * (self.level - 1),
154            })
155            .collect();
156    }
157}
158
159#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, Default)]
160#[tsify(from_wasm_abi)]
161pub struct EquippedPets {
162    pub slotted: BTreeMap<PetSlotId, Pet>,
163}
164
165impl EquippedPets {
166    pub fn new() -> Self {
167        Self {
168            slotted: BTreeMap::new(),
169        }
170    }
171
172    pub fn all_pets(&self) -> Vec<&Pet> {
173        self.slotted.values().collect()
174    }
175
176    pub fn has_pet(&self, pet_id: PetId) -> bool {
177        self.slotted.values().any(|p| p.template_id == pet_id)
178    }
179
180    pub fn get_mut_by_id(&mut self, pet_id: PetId) -> Option<&mut Pet> {
181        self.slotted.values_mut().find(|p| p.template_id == pet_id)
182    }
183}
184
185#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
186pub struct UpgradedPetsMap(pub std::collections::HashMap<PetId, (i64, i64)>);
187
188impl UpgradedPetsMap {
189    pub fn insert(&mut self, id: PetId, levels: (i64, i64)) {
190        self.0.insert(id, levels);
191    }
192}
193
194/// Тип крутки гачи петов.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Tsify)]
196#[tsify(namespace)]
197pub enum PetCaseRollType {
198    Small,
199    Big,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct PetShard {
204    pub pet_id: PetId,
205    pub shards_amount: i64,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
209pub struct PetDrop {
210    pub template: PetTemplate,
211    pub is_new: bool,
212    pub is_checkpoint: bool,
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use uuid::uuid;
219
220    const RARITY_ID: PetRarityId = uuid!("00000000-0000-0000-0000-000000000001");
221    const ATTR_ID_1: AttributeId = uuid!("00000000-0000-0000-0000-00000000000a");
222    const ATTR_ID_2: AttributeId = uuid!("00000000-0000-0000-0000-00000000000b");
223    const PET_ID_1: PetId = uuid!("10000000-0000-0000-0000-000000000001");
224    const PET_ID_2: PetId = uuid!("10000000-0000-0000-0000-000000000002");
225    const PET_ID_3: PetId = uuid!("10000000-0000-0000-0000-000000000003");
226    const RANDOM_ID: PetId = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
227
228    fn make_test_rarity() -> PetRarity {
229        PetRarity {
230            id: RARITY_ID,
231            name: Default::default(),
232            order: 1,
233            color: "#FFD700".to_string(),
234            bg_color: "#1A1A2E".to_string(),
235            icon_path: "rarity/common.png".to_string(),
236            square_icon_path: "rarity/common_square.png".to_string(),
237        }
238    }
239
240    fn make_test_template(id: PetId, stats: Vec<PetSecondaryStat>) -> PetTemplate {
241        PetTemplate {
242            id,
243            name: Default::default(),
244            icon_path: "pet/icon.png".to_string(),
245            spine_path: "pet/spine.json".to_string(),
246            rarity_id: RARITY_ID,
247            stats,
248            is_gacha_pet: false,
249            real_facet: crate::pet_facets::PetFacet::HeadStart,
250            fantasy_facet: crate::pet_facets::PetFacet::SecondSpark,
251        }
252    }
253
254    fn make_two_stats() -> Vec<PetSecondaryStat> {
255        vec![
256            PetSecondaryStat {
257                attribute_id: ATTR_ID_1,
258                base_value: 10,
259                per_level_value: 2,
260            },
261            PetSecondaryStat {
262                attribute_id: ATTR_ID_2,
263                base_value: 50,
264                per_level_value: 5,
265            },
266        ]
267    }
268
269    fn make_pet(id: PetId) -> Pet {
270        let template = make_test_template(id, make_two_stats());
271        Pet::from_template(&template, make_test_rarity(), 1, 0)
272    }
273
274    #[test]
275    fn test_pet_from_template() {
276        let stats = make_two_stats();
277        let template = make_test_template(PET_ID_1, stats);
278        let pet = Pet::from_template(&template, make_test_rarity(), 1, 0);
279
280        assert_eq!(pet.template_id, PET_ID_1);
281        assert_eq!(pet.level, 1);
282        assert_eq!(pet.stats.len(), 2);
283        // At level 1: value = base_value + per_level_value * (1 - 1) = base_value
284        assert_eq!(pet.stats[0].attribute_id, ATTR_ID_1);
285        assert_eq!(pet.stats[0].value, 10);
286        assert_eq!(pet.stats[1].attribute_id, ATTR_ID_2);
287        assert_eq!(pet.stats[1].value, 50);
288    }
289
290    #[test]
291    fn test_pet_from_template_higher_level() {
292        let stats = make_two_stats();
293        let template = make_test_template(PET_ID_1, stats);
294        let pet = Pet::from_template(&template, make_test_rarity(), 5, 10);
295
296        assert_eq!(pet.level, 5);
297        assert_eq!(pet.shards_amount, 10);
298        // At level 5: value = base_value + per_level_value * 4
299        assert_eq!(pet.stats[0].value, 10 + 2 * 4); // 18
300        assert_eq!(pet.stats[1].value, 50 + 5 * 4); // 70
301    }
302
303    #[test]
304    fn test_equipped_pets_has_pet_and_get_mut() {
305        let mut equipped = EquippedPets::new();
306        equipped.slotted.insert(0, make_pet(PET_ID_1));
307
308        assert!(equipped.has_pet(PET_ID_1));
309        assert!(!equipped.has_pet(RANDOM_ID));
310
311        assert!(equipped.get_mut_by_id(PET_ID_1).is_some());
312        assert!(equipped.get_mut_by_id(RANDOM_ID).is_none());
313    }
314
315    #[test]
316    fn test_equipped_pets_multiple_pets() {
317        let mut equipped = EquippedPets::new();
318        equipped.slotted.insert(0, make_pet(PET_ID_1));
319        equipped.slotted.insert(1, make_pet(PET_ID_2));
320        equipped.slotted.insert(2, make_pet(PET_ID_3));
321
322        assert_eq!(equipped.all_pets().len(), 3);
323    }
324}