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