essences/
pets.rs

1use crate::abilities::AbilityId;
2use crate::items::AttributeId;
3use crate::prelude::*;
4
5use std::collections::BTreeMap;
6
7#[declare]
8pub type PetId = Uuid;
9
10#[declare]
11pub type PetSlotId = usize;
12
13#[declare]
14pub type PetRarityId = Uuid;
15
16#[derive(Default, PartialEq, Eq, Hash, Debug, Clone, Serialize, Deserialize, Tsify, JsonSchema)]
17pub struct PetRarity {
18    #[schemars(schema_with = "id_schema")]
19    pub id: PetRarityId,
20
21    #[schemars(title = "Название редкости")]
22    pub name: i18n::I18nString,
23
24    #[schemars(title = "Сортировка")]
25    pub order: u64,
26
27    #[schemars(title = "Цвет редкости", schema_with = "color_schema")]
28    pub color: String,
29
30    #[schemars(title = "Цвет заднего фона редкости", schema_with = "color_schema")]
31    pub bg_color: String,
32
33    #[schemars(title = "Рамка", schema_with = "asset_pet_rarity_icon_schema")]
34    pub icon_path: String,
35
36    #[schemars(
37        title = "Квадратная рамка",
38        schema_with = "asset_pet_rarity_square_icon_schema"
39    )]
40    pub square_icon_path: String,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
44pub struct PetSecondaryStat {
45    #[schemars(title = "Id атрибута", schema_with = "attribute_link_id_schema")]
46    pub attribute_id: AttributeId,
47    #[schemars(title = "Базовое значение")]
48    pub base_value: i64,
49    #[schemars(title = "Значение за уровень")]
50    pub per_level_value: i64,
51}
52
53#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema, Default)]
54#[tsify(from_wasm_abi)]
55pub struct PetTemplate {
56    #[schemars(schema_with = "id_schema")]
57    pub id: PetId,
58
59    #[schemars(title = "Имя пета")]
60    pub name: i18n::I18nString,
61
62    #[schemars(title = "Иконка", schema_with = "asset_pet_icon_schema")]
63    pub icon_path: String,
64
65    #[schemars(title = "Спайн пета", schema_with = "asset_unit_spine_skin")]
66    pub spine_path: String,
67
68    #[schemars(title = "Id редкости", schema_with = "pet_rarity_link_id_schema")]
69    pub rarity_id: PetRarityId,
70
71    #[schemars(title = "Статы пета")]
72    pub stats: Vec<PetSecondaryStat>,
73
74    #[schemars(
75        title = "Id активной способности",
76        schema_with = "option_ability_link_id_schema"
77    )]
78    pub active_ability_id: Option<AbilityId>,
79
80    #[schemars(
81        title = "Id пассивной способности",
82        schema_with = "option_ability_link_id_schema"
83    )]
84    pub passive_ability_id: Option<AbilityId>,
85
86    #[schemars(title = "Скорость зарядки при нанесении урона (basis points)")]
87    pub charge_rate_on_damage_dealt: i64,
88    #[schemars(title = "Скорость зарядки при получении урона (basis points)")]
89    pub charge_rate_on_damage_taken: i64,
90    #[schemars(title = "Скорость зарядки при использовании скилла (basis points)")]
91    pub charge_rate_on_skill_use: i64,
92
93    #[schemars(title = "Максимальный заряд способности")]
94    pub max_charge: i64,
95
96    #[schemars(title = "Показывается в окне гачи, умеет выпадать из гачи")]
97    pub is_gacha_pet: bool,
98}
99
100#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
101pub struct PetComputedSecondaryStat {
102    pub attribute_id: AttributeId,
103    pub value: i64,
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
107#[tsify(from_wasm_abi)]
108pub struct Pet {
109    pub template_id: PetId,
110    pub name: i18n::I18nString,
111    pub icon_path: String,
112    pub rarity: PetRarity,
113    pub level: i64,
114    pub shards_amount: i64,
115    pub active_ability_id: Option<AbilityId>,
116    pub passive_ability_id: Option<AbilityId>,
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            active_ability_id: template.active_ability_id,
144            passive_ability_id: template.passive_ability_id,
145            stats,
146        }
147    }
148
149    pub fn recompute_stats(&mut self, template: &PetTemplate) {
150        self.stats = template
151            .stats
152            .iter()
153            .map(|s| PetComputedSecondaryStat {
154                attribute_id: s.attribute_id,
155                value: s.base_value + s.per_level_value * (self.level - 1),
156            })
157            .collect();
158    }
159}
160
161#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, Default)]
162#[tsify(from_wasm_abi)]
163pub struct EquippedPets {
164    pub slotted: BTreeMap<PetSlotId, Pet>,
165}
166
167impl EquippedPets {
168    pub fn new() -> Self {
169        Self {
170            slotted: BTreeMap::new(),
171        }
172    }
173
174    pub fn leader(&self) -> Option<&Pet> {
175        self.slotted.get(&0)
176    }
177
178    pub fn supports(&self) -> Vec<&Pet> {
179        self.slotted
180            .iter()
181            .filter(|(slot_id, _)| **slot_id != 0)
182            .map(|(_, pet)| pet)
183            .collect()
184    }
185
186    pub fn all_pets(&self) -> Vec<&Pet> {
187        self.slotted.values().collect()
188    }
189
190    pub fn slot_type(slot_id: PetSlotId) -> PetSlotType {
191        if slot_id == 0 {
192            PetSlotType::Leader
193        } else {
194            PetSlotType::Support
195        }
196    }
197
198    pub fn has_pet(&self, pet_id: PetId) -> bool {
199        self.slotted.values().any(|p| p.template_id == pet_id)
200    }
201
202    pub fn get_mut_by_id(&mut self, pet_id: PetId) -> Option<&mut Pet> {
203        self.slotted.values_mut().find(|p| p.template_id == pet_id)
204    }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Tsify)]
208#[tsify(namespace)]
209pub enum PetSlotType {
210    Leader,
211    Support,
212}
213
214#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
215pub struct UpgradedPetsMap(pub std::collections::HashMap<PetId, (i64, i64)>);
216
217impl UpgradedPetsMap {
218    pub fn insert(&mut self, id: PetId, levels: (i64, i64)) {
219        self.0.insert(id, levels);
220    }
221}
222
223/// Тип крутки гачи петов.
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Tsify)]
225#[tsify(namespace)]
226pub enum PetCaseRollType {
227    Small,
228    Big,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct PetShard {
233    pub pet_id: PetId,
234    pub shards_amount: i64,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
238pub struct PetDrop {
239    pub template: PetTemplate,
240    pub is_new: bool,
241    pub is_checkpoint: bool,
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use uuid::uuid;
248
249    const RARITY_ID: PetRarityId = uuid!("00000000-0000-0000-0000-000000000001");
250    const ATTR_ID_1: AttributeId = uuid!("00000000-0000-0000-0000-00000000000a");
251    const ATTR_ID_2: AttributeId = uuid!("00000000-0000-0000-0000-00000000000b");
252    const PET_ID_1: PetId = uuid!("10000000-0000-0000-0000-000000000001");
253    const PET_ID_2: PetId = uuid!("10000000-0000-0000-0000-000000000002");
254    const PET_ID_3: PetId = uuid!("10000000-0000-0000-0000-000000000003");
255    const RANDOM_ID: PetId = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
256
257    fn make_test_rarity() -> PetRarity {
258        PetRarity {
259            id: RARITY_ID,
260            name: Default::default(),
261            order: 1,
262            color: "#FFD700".to_string(),
263            bg_color: "#1A1A2E".to_string(),
264            icon_path: "rarity/common.png".to_string(),
265            square_icon_path: "rarity/common_square.png".to_string(),
266        }
267    }
268
269    fn make_test_template(id: PetId, stats: Vec<PetSecondaryStat>) -> PetTemplate {
270        PetTemplate {
271            id,
272            name: Default::default(),
273            icon_path: "pet/icon.png".to_string(),
274            spine_path: "pet/spine.json".to_string(),
275            rarity_id: RARITY_ID,
276            stats,
277            active_ability_id: None,
278            passive_ability_id: None,
279            charge_rate_on_damage_dealt: 100,
280            charge_rate_on_damage_taken: 50,
281            charge_rate_on_skill_use: 75,
282            max_charge: 1000,
283            is_gacha_pet: false,
284        }
285    }
286
287    fn make_two_stats() -> Vec<PetSecondaryStat> {
288        vec![
289            PetSecondaryStat {
290                attribute_id: ATTR_ID_1,
291                base_value: 10,
292                per_level_value: 2,
293            },
294            PetSecondaryStat {
295                attribute_id: ATTR_ID_2,
296                base_value: 50,
297                per_level_value: 5,
298            },
299        ]
300    }
301
302    fn make_pet(id: PetId) -> Pet {
303        let template = make_test_template(id, make_two_stats());
304        Pet::from_template(&template, make_test_rarity(), 1, 0)
305    }
306
307    #[test]
308    fn test_pet_from_template() {
309        let stats = make_two_stats();
310        let template = make_test_template(PET_ID_1, stats);
311        let pet = Pet::from_template(&template, make_test_rarity(), 1, 0);
312
313        assert_eq!(pet.template_id, PET_ID_1);
314        assert_eq!(pet.level, 1);
315        assert_eq!(pet.stats.len(), 2);
316        // At level 1: value = base_value + per_level_value * (1 - 1) = base_value
317        assert_eq!(pet.stats[0].attribute_id, ATTR_ID_1);
318        assert_eq!(pet.stats[0].value, 10);
319        assert_eq!(pet.stats[1].attribute_id, ATTR_ID_2);
320        assert_eq!(pet.stats[1].value, 50);
321    }
322
323    #[test]
324    fn test_pet_from_template_higher_level() {
325        let stats = make_two_stats();
326        let template = make_test_template(PET_ID_1, stats);
327        let pet = Pet::from_template(&template, make_test_rarity(), 5, 10);
328
329        assert_eq!(pet.level, 5);
330        assert_eq!(pet.shards_amount, 10);
331        // At level 5: value = base_value + per_level_value * 4
332        assert_eq!(pet.stats[0].value, 10 + 2 * 4); // 18
333        assert_eq!(pet.stats[1].value, 50 + 5 * 4); // 70
334    }
335
336    #[test]
337    fn test_equipped_pets_leader_and_supports() {
338        let mut equipped = EquippedPets::new();
339        equipped.slotted.insert(0, make_pet(PET_ID_1));
340        equipped.slotted.insert(1, make_pet(PET_ID_2));
341        equipped.slotted.insert(2, make_pet(PET_ID_3));
342
343        let leader = equipped.leader().expect("should have a leader");
344        assert_eq!(leader.template_id, PET_ID_1);
345
346        let supports = equipped.supports();
347        assert_eq!(supports.len(), 2);
348        assert_eq!(supports[0].template_id, PET_ID_2);
349        assert_eq!(supports[1].template_id, PET_ID_3);
350    }
351
352    #[test]
353    fn test_equipped_pets_has_pet_and_get_mut() {
354        let mut equipped = EquippedPets::new();
355        equipped.slotted.insert(0, make_pet(PET_ID_1));
356
357        assert!(equipped.has_pet(PET_ID_1));
358        assert!(!equipped.has_pet(RANDOM_ID));
359
360        assert!(equipped.get_mut_by_id(PET_ID_1).is_some());
361        assert!(equipped.get_mut_by_id(RANDOM_ID).is_none());
362    }
363
364    #[test]
365    fn test_equipped_pets_multiple_pets() {
366        let mut equipped = EquippedPets::new();
367        equipped.slotted.insert(0, make_pet(PET_ID_1));
368        equipped.slotted.insert(1, make_pet(PET_ID_2));
369        equipped.slotted.insert(2, make_pet(PET_ID_3));
370
371        assert_eq!(equipped.all_pets().len(), 3);
372        assert_eq!(equipped.leader().unwrap().template_id, PET_ID_1);
373        assert_eq!(equipped.supports().len(), 2);
374    }
375}