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(
17    Default,
18    PartialEq,
19    Eq,
20    Hash,
21    Debug,
22    Clone,
23    Serialize,
24    Deserialize,
25    Tsify,
26    JsonSchema,
27    CustomType,
28)]
29pub struct PetRarity {
30    #[schemars(schema_with = "id_schema")]
31    pub id: PetRarityId,
32
33    #[schemars(title = "Название редкости")]
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, CustomType)]
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, CustomType, Default,
67)]
68#[tsify(from_wasm_abi)]
69pub struct PetTemplate {
70    #[schemars(schema_with = "id_schema")]
71    pub id: PetId,
72
73    #[schemars(title = "Имя пета")]
74    pub name: i18n::I18nString,
75
76    #[schemars(title = "Иконка", schema_with = "asset_pet_icon_schema")]
77    pub icon_path: String,
78
79    #[schemars(title = "Спайн пета", schema_with = "asset_unit_spine_skin")]
80    pub spine_path: String,
81
82    #[schemars(title = "Id редкости", schema_with = "pet_rarity_link_id_schema")]
83    pub rarity_id: PetRarityId,
84
85    #[schemars(title = "Статы пета")]
86    pub stats: Vec<PetSecondaryStat>,
87
88    #[schemars(
89        title = "Id активной способности",
90        schema_with = "option_ability_link_id_schema"
91    )]
92    pub active_ability_id: Option<AbilityId>,
93
94    #[schemars(
95        title = "Id пассивной способности",
96        schema_with = "option_ability_link_id_schema"
97    )]
98    pub passive_ability_id: Option<AbilityId>,
99
100    #[schemars(title = "Скорость зарядки при нанесении урона (basis points)")]
101    pub charge_rate_on_damage_dealt: i64,
102    #[schemars(title = "Скорость зарядки при получении урона (basis points)")]
103    pub charge_rate_on_damage_taken: i64,
104    #[schemars(title = "Скорость зарядки при использовании скилла (basis points)")]
105    pub charge_rate_on_skill_use: i64,
106
107    #[schemars(title = "Максимальный заряд способности")]
108    pub max_charge: i64,
109
110    #[schemars(title = "Показывается в окне гачи, умеет выпадать из гачи")]
111    pub is_gacha_pet: bool,
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema, CustomType)]
115pub struct PetComputedSecondaryStat {
116    pub attribute_id: AttributeId,
117    pub value: i64,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, CustomType)]
121#[tsify(from_wasm_abi)]
122pub struct Pet {
123    pub template_id: PetId,
124    pub name: i18n::I18nString,
125    pub icon_path: String,
126    pub rarity: PetRarity,
127    pub level: i64,
128    pub shards_amount: i64,
129    pub active_ability_id: Option<AbilityId>,
130    pub passive_ability_id: Option<AbilityId>,
131    pub stats: Vec<PetComputedSecondaryStat>,
132}
133
134impl Pet {
135    pub fn from_template(
136        template: &PetTemplate,
137        rarity: PetRarity,
138        level: i64,
139        shards_amount: i64,
140    ) -> Self {
141        let stats = template
142            .stats
143            .iter()
144            .map(|s| PetComputedSecondaryStat {
145                attribute_id: s.attribute_id,
146                value: s.base_value + s.per_level_value * (level - 1),
147            })
148            .collect();
149
150        Pet {
151            template_id: template.id,
152            name: template.name.clone(),
153            icon_path: template.icon_path.clone(),
154            rarity,
155            level,
156            shards_amount,
157            active_ability_id: template.active_ability_id,
158            passive_ability_id: template.passive_ability_id,
159            stats,
160        }
161    }
162
163    pub fn recompute_stats(&mut self, template: &PetTemplate) {
164        self.stats = template
165            .stats
166            .iter()
167            .map(|s| PetComputedSecondaryStat {
168                attribute_id: s.attribute_id,
169                value: s.base_value + s.per_level_value * (self.level - 1),
170            })
171            .collect();
172    }
173}
174
175#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, Default)]
176#[tsify(from_wasm_abi)]
177pub struct EquippedPets {
178    pub slotted: BTreeMap<PetSlotId, Pet>,
179}
180
181impl CustomType for EquippedPets {
182    fn build(mut builder: TypeBuilder<Self>) {
183        builder
184            .with_name("EquippedPets")
185            .with_get("slotted", |e: &mut EquippedPets| -> Array {
186                e.slotted
187                    .values()
188                    .cloned()
189                    .map(Dynamic::from)
190                    .collect::<Array>()
191            });
192    }
193}
194
195impl EquippedPets {
196    pub fn new() -> Self {
197        Self {
198            slotted: BTreeMap::new(),
199        }
200    }
201
202    pub fn leader(&self) -> Option<&Pet> {
203        self.slotted.get(&0)
204    }
205
206    pub fn supports(&self) -> Vec<&Pet> {
207        self.slotted
208            .iter()
209            .filter(|(slot_id, _)| **slot_id != 0)
210            .map(|(_, pet)| pet)
211            .collect()
212    }
213
214    pub fn all_pets(&self) -> Vec<&Pet> {
215        self.slotted.values().collect()
216    }
217
218    pub fn slot_type(slot_id: PetSlotId) -> PetSlotType {
219        if slot_id == 0 {
220            PetSlotType::Leader
221        } else {
222            PetSlotType::Support
223        }
224    }
225
226    pub fn has_pet(&self, pet_id: PetId) -> bool {
227        self.slotted.values().any(|p| p.template_id == pet_id)
228    }
229
230    pub fn get_mut_by_id(&mut self, pet_id: PetId) -> Option<&mut Pet> {
231        self.slotted.values_mut().find(|p| p.template_id == pet_id)
232    }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Tsify)]
236#[tsify(namespace)]
237pub enum PetSlotType {
238    Leader,
239    Support,
240}
241
242#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
243pub struct UpgradedPetsMap(pub std::collections::HashMap<PetId, (i64, i64)>);
244
245impl UpgradedPetsMap {
246    pub fn iter(&self) -> rhai::Array {
247        self.0
248            .iter()
249            .map(|(id, (a, b))| {
250                rhai::Dynamic::from(rhai::Array::from(vec![
251                    id.to_string().into(),
252                    (*a).into(),
253                    (*b).into(),
254                ]))
255            })
256            .collect()
257    }
258
259    pub fn insert(&mut self, id: PetId, levels: (i64, i64)) {
260        self.0.insert(id, levels);
261    }
262}
263
264impl CustomType for UpgradedPetsMap {
265    fn build(mut builder: TypeBuilder<Self>) {
266        builder
267            .with_name("UpgradedPetsMap")
268            .with_indexer_get(|ea: &mut UpgradedPetsMap, idx: Uuid| -> rhai::Dynamic {
269                match ea.0.get(&idx) {
270                    Some((a, b)) => rhai::Array::from(vec![(*a).into(), (*b).into()]).into(),
271                    None => ().into(),
272                }
273            })
274            .with_fn("UpgradedPetsMap", Self::default)
275            .with_fn("iter", |map: &mut UpgradedPetsMap| -> rhai::Array {
276                map.iter()
277            });
278    }
279}
280
281/// Тип крутки гачи петов.
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Tsify)]
283#[tsify(namespace)]
284pub enum PetCaseRollType {
285    Small,
286    Big,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct PetShard {
291    pub pet_id: PetId,
292    pub shards_amount: i64,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, CustomType)]
296pub struct PetDrop {
297    pub template: PetTemplate,
298    pub is_new: bool,
299    pub is_checkpoint: bool,
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use uuid::uuid;
306
307    const RARITY_ID: PetRarityId = uuid!("00000000-0000-0000-0000-000000000001");
308    const ATTR_ID_1: AttributeId = uuid!("00000000-0000-0000-0000-00000000000a");
309    const ATTR_ID_2: AttributeId = uuid!("00000000-0000-0000-0000-00000000000b");
310    const PET_ID_1: PetId = uuid!("10000000-0000-0000-0000-000000000001");
311    const PET_ID_2: PetId = uuid!("10000000-0000-0000-0000-000000000002");
312    const PET_ID_3: PetId = uuid!("10000000-0000-0000-0000-000000000003");
313    const RANDOM_ID: PetId = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
314
315    fn make_test_rarity() -> PetRarity {
316        PetRarity {
317            id: RARITY_ID,
318            name: Default::default(),
319            order: 1,
320            color: "#FFD700".to_string(),
321            bg_color: "#1A1A2E".to_string(),
322            icon_path: "rarity/common.png".to_string(),
323            square_icon_path: "rarity/common_square.png".to_string(),
324        }
325    }
326
327    fn make_test_template(id: PetId, stats: Vec<PetSecondaryStat>) -> PetTemplate {
328        PetTemplate {
329            id,
330            name: Default::default(),
331            icon_path: "pet/icon.png".to_string(),
332            spine_path: "pet/spine.json".to_string(),
333            rarity_id: RARITY_ID,
334            stats,
335            active_ability_id: None,
336            passive_ability_id: None,
337            charge_rate_on_damage_dealt: 100,
338            charge_rate_on_damage_taken: 50,
339            charge_rate_on_skill_use: 75,
340            max_charge: 1000,
341            is_gacha_pet: false,
342        }
343    }
344
345    fn make_two_stats() -> Vec<PetSecondaryStat> {
346        vec![
347            PetSecondaryStat {
348                attribute_id: ATTR_ID_1,
349                base_value: 10,
350                per_level_value: 2,
351            },
352            PetSecondaryStat {
353                attribute_id: ATTR_ID_2,
354                base_value: 50,
355                per_level_value: 5,
356            },
357        ]
358    }
359
360    fn make_pet(id: PetId) -> Pet {
361        let template = make_test_template(id, make_two_stats());
362        Pet::from_template(&template, make_test_rarity(), 1, 0)
363    }
364
365    #[test]
366    fn test_pet_from_template() {
367        let stats = make_two_stats();
368        let template = make_test_template(PET_ID_1, stats);
369        let pet = Pet::from_template(&template, make_test_rarity(), 1, 0);
370
371        assert_eq!(pet.template_id, PET_ID_1);
372        assert_eq!(pet.level, 1);
373        assert_eq!(pet.stats.len(), 2);
374        // At level 1: value = base_value + per_level_value * (1 - 1) = base_value
375        assert_eq!(pet.stats[0].attribute_id, ATTR_ID_1);
376        assert_eq!(pet.stats[0].value, 10);
377        assert_eq!(pet.stats[1].attribute_id, ATTR_ID_2);
378        assert_eq!(pet.stats[1].value, 50);
379    }
380
381    #[test]
382    fn test_pet_from_template_higher_level() {
383        let stats = make_two_stats();
384        let template = make_test_template(PET_ID_1, stats);
385        let pet = Pet::from_template(&template, make_test_rarity(), 5, 10);
386
387        assert_eq!(pet.level, 5);
388        assert_eq!(pet.shards_amount, 10);
389        // At level 5: value = base_value + per_level_value * 4
390        assert_eq!(pet.stats[0].value, 10 + 2 * 4); // 18
391        assert_eq!(pet.stats[1].value, 50 + 5 * 4); // 70
392    }
393
394    #[test]
395    fn test_equipped_pets_leader_and_supports() {
396        let mut equipped = EquippedPets::new();
397        equipped.slotted.insert(0, make_pet(PET_ID_1));
398        equipped.slotted.insert(1, make_pet(PET_ID_2));
399        equipped.slotted.insert(2, make_pet(PET_ID_3));
400
401        let leader = equipped.leader().expect("should have a leader");
402        assert_eq!(leader.template_id, PET_ID_1);
403
404        let supports = equipped.supports();
405        assert_eq!(supports.len(), 2);
406        assert_eq!(supports[0].template_id, PET_ID_2);
407        assert_eq!(supports[1].template_id, PET_ID_3);
408    }
409
410    #[test]
411    fn test_equipped_pets_has_pet_and_get_mut() {
412        let mut equipped = EquippedPets::new();
413        equipped.slotted.insert(0, make_pet(PET_ID_1));
414
415        assert!(equipped.has_pet(PET_ID_1));
416        assert!(!equipped.has_pet(RANDOM_ID));
417
418        assert!(equipped.get_mut_by_id(PET_ID_1).is_some());
419        assert!(equipped.get_mut_by_id(RANDOM_ID).is_none());
420    }
421
422    #[test]
423    fn test_equipped_pets_multiple_pets() {
424        let mut equipped = EquippedPets::new();
425        equipped.slotted.insert(0, make_pet(PET_ID_1));
426        equipped.slotted.insert(1, make_pet(PET_ID_2));
427        equipped.slotted.insert(2, make_pet(PET_ID_3));
428
429        assert_eq!(equipped.all_pets().len(), 3);
430        assert_eq!(equipped.leader().unwrap().template_id, PET_ID_1);
431        assert_eq!(equipped.supports().len(), 2);
432    }
433}