essences/
characters.rs

1use crate::abilities::AbilityId;
2use crate::autochest::AutoChestFilterId;
3use crate::class::ClassId;
4use crate::pets::PetId;
5use crate::talent_tree::TalentId;
6
7use rand::Rng;
8
9use super::types::CustomValuesMap;
10
11use crate::prelude::*;
12use strum_macros::{Display, EnumString};
13
14#[tsify_next::declare]
15pub type CharacterId = uuid::Uuid;
16
17#[derive(
18    Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, EnumString, Display,
19)]
20pub enum FightClass {
21    Mage,
22    Warrior,
23    Archer,
24}
25
26impl CustomType for FightClass {
27    fn build(mut builder: TypeBuilder<Self>) {
28        builder
29            .with_name("FightClass")
30            .with_get("str", |vts: &mut FightClass| FightClass::to_string(vts));
31    }
32}
33
34#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, Tsify, CustomType)]
35pub struct Character {
36    pub id: CharacterId,
37    pub user_id: uuid::Uuid,
38    pub created_at: chrono::DateTime<chrono::Utc>,
39    pub class: ClassId,
40    pub character_level: i64,
41    pub character_experience: i64,
42    pub patron_level: i64,
43    pub patron_experience: i64,
44    pub cases: i64, // TODO remove
45    pub current_chapter_level: i64,
46    pub current_fight_number: i64,
47    pub max_vassal_slots_count: i64,
48    pub custom_values: CustomValuesMap,
49    pub item_case_level: i64,
50    /// Текущий уровень гачи способностей.
51    pub ability_case_level: i64,
52    /// Накопленные очки прогресса гачи способностей.
53    /// Историческое имя поля сохранено для обратной совместимости.
54    pub opened_ability_cases: i64,
55    /// Выбранные шаблоны способностей для вишлиста гачи.
56    pub ability_gacha_wishlist: Vec<AbilityId>,
57    /// Уровни слотов способностей по индексам `slot_id`.
58    pub ability_slot_levels: Vec<i64>,
59    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
60    pub item_case_upgrade_finish_at: Option<chrono::DateTime<chrono::Utc>>,
61    pub power: i64,
62    pub fight_class: Option<FightClass>,
63    pub arena_rating: i64,
64    pub arena_stars: i64,
65    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
66    pub arena_last_pvp_at: Option<chrono::DateTime<chrono::Utc>>,
67    pub last_boss_fight_won: bool,
68    pub auto_chest_enabled: bool,
69    pub last_filter_used_id: Option<AutoChestFilterId>,
70    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
71    pub last_item_case_speedup_at: Option<chrono::DateTime<chrono::Utc>>,
72    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
73    pub last_afk_reward_claimed_at: chrono::DateTime<chrono::Utc>,
74    pub afk_reward_seed: u64,
75    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
76    pub last_gacha_ad_roll_at: Option<chrono::DateTime<chrono::Utc>>,
77    /// Number of chest upgrade speedups used today via watching an ad.
78    pub speedup_ad_daily_count: i64,
79    pub active_loop_task_id: Option<uuid::Uuid>,
80    pub purchases_banned: bool,
81    pub completed_tutorials: Vec<i16>,
82    /// Уровни слотов петов по индексам `slot_id`.
83    pub pet_slot_levels: Vec<i64>,
84    /// Текущий уровень гачи петов.
85    pub pet_case_level: i64,
86    /// Накопленные очки прогресса гачи петов.
87    pub opened_pet_cases: i64,
88    /// Выбранные шаблоны петов для вишлиста гачи.
89    pub pet_gacha_wishlist: Vec<PetId>,
90    /// ID персонажа, добавленного в пати.
91    pub party_character_id: Option<uuid::Uuid>,
92    // TODO: add max fight id (not double prize)
93    /// ID таланта, который сейчас изучается (если есть).
94    pub talent_upgrading_id: Option<TalentId>,
95    /// Время завершения изучения таланта.
96    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
97    pub talent_upgrade_finish_at: Option<chrono::DateTime<chrono::Utc>>,
98    /// Время последнего активного соединения игрока (обновляется каждый тик).
99    pub last_online_at: chrono::DateTime<chrono::Utc>,
100    /// Количество нажатий на мгновенную награду AFK за гемы за сегодня.
101    pub instant_reward_gems_press_count: i64,
102    /// Накопленные стаки буста AFK-награды от просмотра рекламы.
103    /// Применяются к следующему клейму AFK-награды и сбрасываются.
104    pub afk_boost_pending_stacks: i64,
105    /// Время, до которого нельзя показывать следующую птицу.
106    /// Короткий кулдаун устанавливается при эмите `ShowBird` сервером,
107    /// полный — при подтверждении показа клиентом (`BirdShown`).
108    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
109    pub bird_cooldown_until: Option<chrono::DateTime<chrono::Utc>>,
110}
111
112impl PartialEq for Character {
113    fn eq(&self, other: &Self) -> bool {
114        // Compare with microsecond precision because cockroach has microsecond precision.
115        // Helps in tests comparing, because cockroach has microsecond precision
116        fn eq_system_time(
117            a: &chrono::DateTime<chrono::Utc>,
118            b: &chrono::DateTime<chrono::Utc>,
119        ) -> bool {
120            a.signed_duration_since(*b).abs() < chrono::TimeDelta::microseconds(1)
121        }
122
123        fn eq_opt_time(
124            a: &Option<chrono::DateTime<chrono::Utc>>,
125            b: &Option<chrono::DateTime<chrono::Utc>>,
126        ) -> bool {
127            match (a, b) {
128                (Some(x), Some(y)) => eq_system_time(x, y),
129                (None, None) => true,
130                _ => false,
131            }
132        }
133
134        let Character {
135            id,
136            user_id,
137            class,
138            character_level,
139            character_experience,
140            patron_level,
141            patron_experience,
142            cases,
143            current_chapter_level,
144            current_fight_number,
145            max_vassal_slots_count,
146            custom_values,
147            item_case_level,
148            ability_case_level,
149            opened_ability_cases,
150            item_case_upgrade_finish_at,
151            power,
152            fight_class,
153            arena_rating,
154            arena_stars,
155            arena_last_pvp_at,
156            last_boss_fight_won,
157            auto_chest_enabled,
158            last_filter_used_id,
159            last_item_case_speedup_at,
160            last_afk_reward_claimed_at,
161            afk_reward_seed,
162            last_gacha_ad_roll_at,
163            speedup_ad_daily_count,
164            active_loop_task_id,
165            purchases_banned,
166            created_at,
167            ability_gacha_wishlist,
168            ability_slot_levels,
169            completed_tutorials,
170            pet_slot_levels,
171            pet_case_level,
172            opened_pet_cases,
173            pet_gacha_wishlist,
174            party_character_id,
175            talent_upgrading_id,
176            talent_upgrade_finish_at,
177            last_online_at,
178            instant_reward_gems_press_count,
179            afk_boost_pending_stacks,
180            bird_cooldown_until,
181        } = self;
182
183        let Character {
184            id: o_id,
185            user_id: o_user_id,
186            class: o_class,
187            character_level: o_character_level,
188            character_experience: o_character_experience,
189            patron_level: o_patron_level,
190            patron_experience: o_patron_experience,
191            cases: o_cases,
192            current_chapter_level: o_current_chapter_level,
193            current_fight_number: o_current_fight_number,
194            max_vassal_slots_count: o_max_vassal_slots_count,
195            custom_values: o_custom_values,
196            item_case_level: o_item_case_level,
197            ability_case_level: o_ability_case_level,
198            opened_ability_cases: o_opened_ability_cases,
199            item_case_upgrade_finish_at: o_item_case_upgrade_finish_at,
200            power: o_power,
201            fight_class: o_fight_class,
202            arena_rating: o_arena_rating,
203            arena_stars: o_arena_stars,
204            arena_last_pvp_at: o_arena_last_pvp_at,
205            last_boss_fight_won: o_last_boss_fight_won,
206            auto_chest_enabled: o_auto_chest_enabled,
207            last_filter_used_id: o_last_filter_used_id,
208            last_item_case_speedup_at: o_last_item_case_speedup_at,
209            last_afk_reward_claimed_at: o_last_afk_reward_claimed_at,
210            afk_reward_seed: o_afk_reward_seed,
211            last_gacha_ad_roll_at: o_last_gacha_ad_roll_at,
212            speedup_ad_daily_count: o_speedup_ad_daily_count,
213            active_loop_task_id: o_active_loop_task_id,
214            purchases_banned: o_purchases_banned,
215            created_at: o_created_at,
216            ability_gacha_wishlist: o_ability_gacha_wishlist,
217            ability_slot_levels: o_ability_slot_levels,
218            completed_tutorials: o_completed_tutorials,
219            pet_slot_levels: o_pet_slot_levels,
220            pet_case_level: o_pet_case_level,
221            opened_pet_cases: o_opened_pet_cases,
222            pet_gacha_wishlist: o_pet_gacha_wishlist,
223            party_character_id: o_party_character_id,
224            talent_upgrading_id: o_talent_upgrading_id,
225            talent_upgrade_finish_at: o_talent_upgrade_finish_at,
226            last_online_at: o_last_online_at,
227            instant_reward_gems_press_count: o_instant_reward_gems_press_count,
228            afk_boost_pending_stacks: o_afk_boost_pending_stacks,
229            bird_cooldown_until: o_bird_cooldown_until,
230        } = other;
231
232        id == o_id
233            && user_id == o_user_id
234            && class == o_class
235            && character_level == o_character_level
236            && character_experience == o_character_experience
237            && patron_level == o_patron_level
238            && patron_experience == o_patron_experience
239            && cases == o_cases
240            && current_chapter_level == o_current_chapter_level
241            && current_fight_number == o_current_fight_number
242            && max_vassal_slots_count == o_max_vassal_slots_count
243            && custom_values == o_custom_values
244            && item_case_level == o_item_case_level
245            && ability_case_level == o_ability_case_level
246            && opened_ability_cases == o_opened_ability_cases
247            && eq_opt_time(item_case_upgrade_finish_at, o_item_case_upgrade_finish_at)
248            && power == o_power
249            && fight_class == o_fight_class
250            && arena_rating == o_arena_rating
251            && arena_stars == o_arena_stars
252            && eq_opt_time(arena_last_pvp_at, o_arena_last_pvp_at)
253            && last_boss_fight_won == o_last_boss_fight_won
254            && auto_chest_enabled == o_auto_chest_enabled
255            && last_filter_used_id == o_last_filter_used_id
256            && eq_opt_time(last_item_case_speedup_at, o_last_item_case_speedup_at)
257            && eq_system_time(last_afk_reward_claimed_at, o_last_afk_reward_claimed_at)
258            && afk_reward_seed == o_afk_reward_seed
259            && eq_opt_time(last_gacha_ad_roll_at, o_last_gacha_ad_roll_at)
260            && speedup_ad_daily_count == o_speedup_ad_daily_count
261            && active_loop_task_id == o_active_loop_task_id
262            && purchases_banned == o_purchases_banned
263            && completed_tutorials == o_completed_tutorials
264            && pet_slot_levels == o_pet_slot_levels
265            && pet_case_level == o_pet_case_level
266            && opened_pet_cases == o_opened_pet_cases
267            && pet_gacha_wishlist == o_pet_gacha_wishlist
268            && party_character_id == o_party_character_id
269            && talent_upgrading_id == o_talent_upgrading_id
270            && eq_opt_time(talent_upgrade_finish_at, o_talent_upgrade_finish_at)
271            && eq_system_time(last_online_at, o_last_online_at)
272            && eq_system_time(created_at, o_created_at)
273            && ability_gacha_wishlist == o_ability_gacha_wishlist
274            && ability_slot_levels == o_ability_slot_levels
275            && instant_reward_gems_press_count == o_instant_reward_gems_press_count
276            && afk_boost_pending_stacks == o_afk_boost_pending_stacks
277            && eq_opt_time(bird_cooldown_until, o_bird_cooldown_until)
278    }
279}
280
281impl Eq for Character {}
282
283impl Default for Character {
284    fn default() -> Self {
285        Self {
286            id: Default::default(),
287            user_id: Default::default(),
288            created_at: ::time::utc_now(),
289            class: Default::default(),
290            character_level: 0,
291            character_experience: 0,
292            patron_level: 0,
293            patron_experience: 0,
294            cases: 0,
295            current_chapter_level: 0,
296            current_fight_number: 0,
297            max_vassal_slots_count: 0,
298            custom_values: Default::default(),
299            item_case_level: 0,
300            ability_case_level: 0,
301            opened_ability_cases: 0,
302            ability_gacha_wishlist: Vec::new(),
303            ability_slot_levels: Vec::new(),
304            item_case_upgrade_finish_at: None,
305            power: 0,
306            fight_class: None,
307            arena_rating: 0,
308            arena_stars: 0,
309            arena_last_pvp_at: None,
310            last_boss_fight_won: false,
311            auto_chest_enabled: false,
312            last_filter_used_id: None,
313            last_item_case_speedup_at: None,
314            last_afk_reward_claimed_at: ::time::utc_now(),
315            afk_reward_seed: 0,
316            last_gacha_ad_roll_at: None,
317            speedup_ad_daily_count: 0,
318            active_loop_task_id: None,
319            purchases_banned: false,
320            completed_tutorials: Default::default(),
321            pet_slot_levels: Vec::new(),
322            pet_case_level: 1,
323            opened_pet_cases: 0,
324            pet_gacha_wishlist: Vec::new(),
325            party_character_id: None,
326            talent_upgrading_id: None,
327            talent_upgrade_finish_at: None,
328            last_online_at: ::time::utc_now(),
329            instant_reward_gems_press_count: 0,
330            afk_boost_pending_stacks: 0,
331            bird_cooldown_until: None,
332        }
333    }
334}
335
336// Seed from 0 to i64::MAX, because cockroach can't store unsigned
337pub fn generate_new_afk_seed() -> u64 {
338    let mut rng = rand::rng();
339    rng.random_range(0..=i64::MAX as u64)
340}
341
342#[derive(Default, Serialize, Deserialize)]
343pub struct CharacterBuilder {
344    character: Character,
345}
346
347impl CharacterBuilder {
348    pub fn new() -> Self {
349        Self {
350            character: Character {
351                id: Uuid::now_v7(),
352                character_level: 1,
353                patron_level: 1,
354                item_case_level: 1,
355                ability_case_level: 1,
356                pet_case_level: 1,
357                max_vassal_slots_count: 2,
358                last_boss_fight_won: true,
359                power: 1,
360                afk_reward_seed: generate_new_afk_seed(),
361                ..Default::default()
362            },
363        }
364    }
365
366    pub fn with_id(mut self, id: CharacterId) -> Self {
367        self.character.id = id;
368        self
369    }
370
371    pub fn with_user_id(mut self, id: Uuid) -> Self {
372        self.character.user_id = id;
373        self
374    }
375
376    pub fn with_power(mut self, power: i64) -> Self {
377        self.character.power = power;
378        self
379    }
380
381    pub fn with_max_vassal_slots_count(mut self, slots_count: i64) -> Self {
382        self.character.max_vassal_slots_count = slots_count;
383        self
384    }
385
386    pub fn with_arena_rating(mut self, rating: i64) -> Self {
387        self.character.arena_rating = rating;
388        self
389    }
390
391    pub fn with_arena_stars(mut self, stars: i64) -> Self {
392        self.character.arena_stars = stars;
393        self
394    }
395
396    pub fn with_character_level(mut self, level: i64) -> Self {
397        self.character.character_level = level;
398        self
399    }
400
401    pub fn with_arena_last_pvp_at(mut self, time: Option<chrono::DateTime<chrono::Utc>>) -> Self {
402        self.character.arena_last_pvp_at = time;
403        self
404    }
405
406    pub fn with_last_boss_fight_won(mut self, won: bool) -> Self {
407        self.character.last_boss_fight_won = won;
408        self
409    }
410
411    pub fn with_cases(mut self, cases: i64) -> Self {
412        self.character.cases = cases;
413        self
414    }
415
416    pub fn with_item_case_level(mut self, level: i64) -> Self {
417        self.character.item_case_level = level;
418        self
419    }
420
421    pub fn with_class(mut self, class_id: Uuid) -> Self {
422        self.character.class = class_id;
423        self
424    }
425
426    pub fn with_custom_values(mut self, custom_values: CustomValuesMap) -> Self {
427        self.character.custom_values = custom_values;
428        self
429    }
430
431    pub fn with_ability_slot_levels(mut self, ability_slot_levels: Vec<i64>) -> Self {
432        self.character.ability_slot_levels = ability_slot_levels;
433        self
434    }
435
436    pub fn with_ability_gacha_wishlist(mut self, ability_gacha_wishlist: Vec<AbilityId>) -> Self {
437        self.character.ability_gacha_wishlist = ability_gacha_wishlist;
438        self
439    }
440
441    pub fn with_seed(mut self, seed: u64) -> Self {
442        self.character.afk_reward_seed = seed;
443        self
444    }
445
446    pub fn with_current_chapter_level(mut self, chapter_level: i64) -> Self {
447        self.character.current_chapter_level = chapter_level;
448        self
449    }
450
451    pub fn build(self) -> Character {
452        self.character
453    }
454}