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