1use essences::abilities::{AbilityRarity, AbilityTemplate};
2use essences::ability_presets::AbilityPresetsSettings;
3use essences::bundles::BundleRaw;
4use essences::chats::ChatSettings;
5use essences::class;
6use essences::currency::Currency;
7use essences::dungeons::DungeonTemplate;
8use essences::effect::Effect;
9use essences::fighting::FightTemplate;
10use essences::game::{AbilitySlotsLevel, Chapter, CharacterLevel, EntityTemplate, PetSlotsLevel};
11use essences::gatings::Gatings;
12use essences::generation::{BotsSettings, UsersGeneratingSettings};
13use essences::gift::GiftTemplate;
14use essences::item_case::{InventoryLevel, ItemCasesSettingsByLevel};
15use essences::items::{Attribute, ItemRarity, ItemTemplate};
16use essences::mail::MailTemplate;
17use essences::offers::{OfferTemplate, ShopTabConfig};
18use essences::pets::{PetRarity, PetTemplate};
19use essences::progress_pass::ProgressPassConfig;
20use essences::quest::{QuestGroupType, QuestTemplate, QuestsProgressionSettings};
21use essences::ratings::RatingSettings;
22use essences::referrals::ReferralLevelInfo;
23use essences::skins::{ConfigSkin, SkinsSettings};
24use essences::vassals::VassalTaskTemplate;
25use schemars::JsonSchema;
26
27use serde::{Deserialize, Serialize};
28use tsify_next::Tsify;
29
30use crate::abilities::{AbilityCasesSettingsByLevel, AbilityLevel, Projectile};
31use crate::ads_settings::{AdsSettings, BirdVariant};
32use crate::afk_rewards::{AfkRewardBonusType, AfkRewardsByLevel, AfkRewardsSettings};
33use crate::buffs::BuffTemplate;
34use crate::cheats::{CheatScript, TestPlayerScript};
35use crate::events::EventDescription;
36use crate::fighting::{ArenaLeague, ArenaSettings, FightSettings, PvpSettings};
37use crate::game_settings::GameSettings;
38use crate::matchmaking::MatchmakingSettings;
39use crate::pets::{PetCasesSettingsByLevel, PetLevel};
40use crate::reports::ReportsSettings;
41use crate::statue::{StatueBonusTypeConfig, StatueLevelConfig, StatueSettings};
42use crate::tutorial::TutorialStep;
43use crate::validated_types::NonEmptyVec;
44use crate::vassals::VassalsSettings;
45use essences::statue::StatueBonusGrade;
46use essences::talent_tree::{TalentTemplate, TalentTreeSettings};
47
48#[derive(Clone, Serialize, Deserialize, JsonSchema, Tsify)]
49#[tsify(into_wasm_abi)]
50pub struct GameConfig {
51 #[schemars(title = "Атрибуты")]
52 pub attributes: Vec<Attribute>,
53
54 #[schemars(title = "Настройки сундука-гачи-айтемов")]
55 pub item_cases_settings: Vec<ItemCasesSettingsByLevel>,
56
57 #[schemars(
58 title = "Настройки игры",
59 description = "Отдельные значения, которые используются в игре"
60 )]
61 pub game_settings: GameSettings,
62
63 #[schemars(
64 title = "Настройки действий за рекламу",
65 description = "Все настройки действий, связанные с рекламой"
66 )]
67 pub ads_settings: AdsSettings,
68
69 #[schemars(title = "Предметы")]
70 pub items: Vec<ItemTemplate>,
71
72 #[schemars(title = "Редкости предметов")]
73 pub item_rarities: Vec<ItemRarity>,
74
75 #[schemars(title = "Скины")]
76 pub skins: Vec<ConfigSkin>,
77
78 #[schemars(title = "Настройки скинов для кастомизации")]
79 pub skins_settings: SkinsSettings,
80
81 #[schemars(title = "Эффекты")]
82 pub effects: Vec<Effect>,
83
84 #[schemars(title = "Способности")]
85 pub abilities: Vec<AbilityTemplate>,
86
87 #[schemars(title = "Настройки сундука-гачи-скилов")]
88 pub ability_cases_settings: NonEmptyVec<AbilityCasesSettingsByLevel>,
89
90 #[schemars(title = "Редкости способностей")]
91 pub ability_rarities: Vec<AbilityRarity>,
92
93 #[schemars(title = "Уровни способностей")]
94 pub ability_levels: Vec<AbilityLevel>,
95
96 #[schemars(title = "Настройки боя")]
97 pub fight_settings: FightSettings,
98
99 #[schemars(title = "Враги")]
100 pub entities: Vec<EntityTemplate>,
101
102 #[schemars(title = "Шаблоны данжей")]
103 pub dungeon_templates: Vec<DungeonTemplate>,
104
105 #[schemars(title = "Шаблоны боев")]
106 pub fight_templates: Vec<FightTemplate>,
107
108 #[schemars(title = "Главы кампании")]
109 pub chapters: Vec<Chapter>,
110
111 #[schemars(title = "Уровни игрока")]
112 pub character_levels: Vec<CharacterLevel>,
113
114 #[schemars(title = "Уровни слотов способностей")]
115 pub ability_slots_levels: Vec<AbilitySlotsLevel>,
116
117 #[schemars(title = "Реферальные уровни игрока")]
118 pub patron_levels: Vec<ReferralLevelInfo>,
119
120 #[schemars(title = "Квесты")]
121 pub quests: Vec<QuestTemplate>,
122
123 #[schemars(title = "Настройки прогрессии квестов")]
124 pub quests_progression_settings: QuestsProgressionSettings,
125
126 #[schemars(title = "Настройки вассалов")]
127 pub vassals_settings: VassalsSettings,
128
129 #[schemars(title = "Поручения")]
130 pub vassal_tasks: Vec<VassalTaskTemplate>,
131
132 #[schemars(title = "Настройки PvP")]
133 pub pvp_settings: PvpSettings,
134
135 #[schemars(title = "Подарки")]
136 pub gifts: Vec<GiftTemplate>,
137
138 #[schemars(title = "Шаблоны писем")]
139 pub mail_templates: Vec<MailTemplate>,
140
141 #[schemars(title = "Валюты")]
142 pub currencies: Vec<Currency>,
143
144 #[schemars(title = "Уровни инвентаря")]
145 pub inventory_levels: Vec<InventoryLevel>,
146
147 #[schemars(title = "Настройки арены")]
148 pub arena_settings: ArenaSettings,
149
150 #[schemars(title = "Настройки лиг")]
151 pub arena_leagues: Vec<ArenaLeague>,
152
153 #[schemars(title = "Настройки матчмейкинга")]
154 pub matchmaking_settings: MatchmakingSettings,
155
156 #[schemars(title = "Описания эвентов")]
157 pub event_descriptions: Vec<EventDescription>,
158
159 #[schemars(title = "Классы персонажа")]
160 pub classes: Vec<class::Class>,
161
162 #[schemars(title = "Уровни прокачки классов")]
163 pub class_levels: Vec<class::ClassLevels>,
164
165 #[schemars(title = "Список проджектайлов")]
166 pub projectiles: Vec<Projectile>,
167
168 #[schemars(title = "Список бандлов")]
169 pub bundles: Vec<BundleRaw>,
170
171 #[schemars(title = "Настройки пресетов способностей")]
172 pub ability_presets_settings: AbilityPresetsSettings,
173
174 #[schemars(title = "Настройки ботов")]
175 pub bots_settings: BotsSettings,
176
177 #[schemars(title = "Настройки репортов")]
178 pub reports_settings: ReportsSettings,
179
180 #[schemars(title = "Настройки афк наград")]
181 pub afk_rewards_settings: AfkRewardsSettings,
182
183 #[schemars(title = "Уровни афк наград")]
184 pub afk_rewards_levels: Vec<AfkRewardsByLevel>,
185
186 #[schemars(title = "Настройки генерации параметров пользователя")]
187 pub users_generating_settings: UsersGeneratingSettings,
188
189 #[schemars(title = "Шаги туториала")]
190 pub tutorial_steps: Vec<TutorialStep>,
191
192 #[schemars(title = "Настройки чатов")]
193 pub chats_settings: Vec<ChatSettings>,
194
195 #[schemars(title = "Шаблоны офферов")]
196 pub offers_templates: Vec<OfferTemplate>,
197
198 #[schemars(title = "Вкладки магазина")]
199 pub shop_tabs: Vec<ShopTabConfig>,
200
201 #[schemars(title = "Настройки рейтингов")]
202 pub ratings_settings: Vec<RatingSettings>,
203
204 #[schemars(title = "Набор скриптов для читов")]
205 pub cheat_scripts: Vec<CheatScript>,
206
207 #[schemars(
208 title = "Скрипты генерации тестового игрока",
209 description = "Используется только в режиме читов. Каждый скрипт в формате TestPlayerResult — задаёт предметы, способности, петов, уровень, класс и силу тестового игрока. Выбирается по индексу."
210 )]
211 pub test_player_scripts: Vec<TestPlayerScript>,
212
213 #[schemars(title = "Гейтинги")]
214 pub gatings: Gatings,
215
216 #[schemars(title = "Шаблоны петов")]
217 pub pet_templates: Vec<PetTemplate>,
218
219 #[schemars(title = "Редкости петов")]
220 pub pet_rarities: Vec<PetRarity>,
221
222 #[schemars(title = "Уровни петов")]
223 pub pet_levels: Vec<PetLevel>,
224
225 #[schemars(title = "Уровни слотов петов")]
226 pub pet_slots_levels: Vec<PetSlotsLevel>,
227
228 #[schemars(title = "Настройки сундука-гачи-петов")]
229 pub pet_cases_settings: Vec<PetCasesSettingsByLevel>,
230
231 #[schemars(title = "Настройки дерева талантов")]
232 pub talent_tree_settings: TalentTreeSettings,
233
234 #[schemars(title = "Таланты")]
235 pub talents: Vec<TalentTemplate>,
236
237 #[schemars(title = "Грейды бонусов статуи")]
238 pub statue_bonus_grades: NonEmptyVec<StatueBonusGrade>,
239
240 #[schemars(title = "Типы бонусов статуи (стат × грейд)")]
241 pub statue_bonus_type_configs: NonEmptyVec<StatueBonusTypeConfig>,
242
243 #[schemars(title = "Таблица уровней статуи")]
244 pub statue_level_configs: NonEmptyVec<StatueLevelConfig>,
245
246 #[schemars(title = "Настройки статуи героя")]
247 pub statue_settings: StatueSettings,
248
249 #[schemars(title = "Шаблоны баффов")]
250 pub buff_templates: Vec<BuffTemplate>,
251
252 #[schemars(title = "Настройки прогресс пасса")]
253 pub progress_pass: ProgressPassConfig,
254
255 #[schemars(title = "Варианты рекламных птиц")]
256 pub bird_variants: Vec<BirdVariant>,
257}
258
259impl std::fmt::Debug for GameConfig {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 write!(f, "GameConfig")
262 }
263}
264
265impl GameConfig {
266 pub fn clone_translate(&self, translator: &i18n::translator::Translator, locale: &str) -> Self {
267 let mut config = self.clone();
268 for ability in &mut config.abilities {
269 ability.name = translator.translate(&ability.name, locale);
270 ability.description = translator.translate(&ability.description, locale);
271 }
272
273 for attribute in &mut config.attributes {
274 attribute.name = translator.translate(&attribute.name, locale);
275 attribute.description = translator.translate(&attribute.description, locale);
276 if let Some(prefix) = &attribute.prefix {
277 attribute.prefix = Some(translator.translate(prefix, locale));
278 }
279 if let Some(suffix) = &attribute.suffix {
280 attribute.suffix = Some(translator.translate(suffix, locale));
281 }
282 }
283
284 for effect in &mut config.effects {
285 effect.name = translator.translate(&effect.name, locale);
286 }
287
288 for event in &mut config.event_descriptions {
289 event.description = translator.translate(&event.description, locale);
290 }
291
292 for item in &mut config.items {
293 item.name = translator.translate(&item.name, locale);
294 }
295
296 for item_rarity in &mut config.item_rarities {
297 item_rarity.name = translator.translate(&item_rarity.name, locale);
298 }
299
300 for skin in &mut config.skins {
301 skin.name = translator.translate(&skin.name, locale);
302 }
303
304 for ability_rarity in &mut config.ability_rarities {
305 ability_rarity.name = translator.translate(&ability_rarity.name, locale);
306 }
307
308 for quest in &mut config.quests {
309 quest.title = translator.translate(&quest.title, locale);
310 quest.description = translator.translate(&quest.description, locale);
311 }
312
313 for vassal_task in &mut config.vassal_tasks {
314 vassal_task.title = translator.translate(&vassal_task.title, locale);
315 }
316
317 for currency in &mut config.currencies {
318 currency.name = translator.translate(¤cy.name, locale);
319 currency.description = translator.translate(¤cy.description, locale);
320 }
321
322 for mail_template in &mut config.mail_templates {
323 mail_template.title = translator.translate(&mail_template.title, locale);
324 mail_template.message = translator.translate(&mail_template.message, locale);
325 }
326
327 for league in &mut config.arena_leagues {
328 league.name = translator.translate(&league.name, locale);
329 }
330
331 for fight in &mut config.fight_templates {
332 fight.title = translator.translate(&fight.title, locale);
333 }
334
335 for class in &mut config.classes {
336 class.name = translator.translate(&class.name, locale);
337 class.description = translator.translate(&class.description, locale);
338 class.weapon_type = translator.translate(&class.weapon_type, locale);
339 }
340
341 for dungeon in &mut config.dungeon_templates {
342 dungeon.title = translator.translate(&dungeon.title, locale);
343 dungeon.description = translator.translate(&dungeon.description, locale);
344
345 for tip in &mut dungeon.tips {
346 tip.tip = translator.translate(&tip.tip, locale);
347 }
348 }
349
350 for step in &mut config.tutorial_steps {
351 step.text = translator.translate(&step.text, locale);
352 }
353
354 for offer in &mut config.offers_templates {
355 offer.title = translator.translate(&offer.title, locale);
356 if let Some(limit_buy_text) = &offer.limit_buy_text {
357 offer.limit_buy_text = Some(translator.translate(limit_buy_text, locale));
358 }
359 }
360
361 for shop_tab in &mut config.shop_tabs {
362 shop_tab.name = translator.translate(&shop_tab.name, locale);
363 }
364
365 for chapter in &mut config.chapters {
366 chapter.title = translator.translate(&chapter.title, locale);
367 }
368
369 for pet_template in &mut config.pet_templates {
370 pet_template.name = translator.translate(&pet_template.name, locale);
371 }
372
373 for pet_rarity in &mut config.pet_rarities {
374 pet_rarity.name = translator.translate(&pet_rarity.name, locale);
375 }
376
377 for talent in &mut config.talents {
378 talent.name = translator.translate(&talent.name, locale);
379 talent.description = translator.translate(&talent.description, locale);
380 }
381
382 config.ability_presets_settings.default_preset_name =
383 translator.translate(&config.ability_presets_settings.default_preset_name, locale);
384
385 for grade in &mut config.statue_bonus_grades {
386 grade.name = translator.translate(&grade.name, locale);
387 }
388
389 for buff in &mut config.buff_templates {
390 buff.title = translator.translate(&buff.title, locale);
391 buff.description = translator.translate(&buff.description, locale);
392 }
393
394 for skin in &mut config.skins {
395 if let Some(unlock_description) = &skin.unlock_description {
396 skin.unlock_description = Some(translator.translate(unlock_description, locale));
397 }
398 }
399
400 config
401 }
402
403 pub fn validate(&self) {
404 if !Self::has_unique_ids(&self.items) {
406 panic!("Not unique item ids in config");
407 }
408
409 self.validate_ability_gacha_settings();
410
411 Self::validate_afk_rewards(
412 &self.afk_rewards_levels,
413 &self.afk_rewards_settings,
414 &self.currencies,
415 &self.abilities,
416 &self.items,
417 &self.bundles,
418 );
419 Self::validate_loop_task_chain(&self.quests);
420 self.validate_pet_config();
421 self.validate_pet_gacha_settings();
422 self.validate_statue_settings();
423 self.validate_class_levels();
424 self.validate_class_abilities();
425 Self::validate_progress_pass_config(&self.progress_pass);
426 self.validate_progress_pass_references();
427 }
428
429 fn validate_class_abilities(&self) {
434 use std::collections::HashSet;
435
436 for class in &self.classes {
437 let basics: HashSet<_> = class.basic_abilities.iter().copied().collect();
438 let mut seen = HashSet::new();
439 for ability_id in &class.class_abilities {
440 if !seen.insert(*ability_id) {
441 panic!(
442 "Class {}: duplicate ability_id {ability_id} in class_abilities",
443 class.id
444 );
445 }
446 if basics.contains(ability_id) {
447 panic!(
448 "Class {}: ability {ability_id} appears in both basic_abilities and class_abilities",
449 class.id
450 );
451 }
452 let Some(template) = self.abilities.iter().find(|a| a.id == *ability_id) else {
453 panic!(
454 "Class {}: class_abilities references unknown ability_id {ability_id}",
455 class.id
456 );
457 };
458 if template.is_gacha_ability {
459 panic!(
460 "Class {}: class_ability {ability_id} has is_gacha_ability=true; class actives must be excluded from the gacha pool",
461 class.id
462 );
463 }
464 }
465 }
466 }
467
468 fn validate_class_levels(&self) {
469 use std::collections::HashMap;
470
471 let mut by_class: HashMap<class::ClassId, Vec<u64>> = HashMap::new();
472 for row in &self.class_levels {
473 by_class.entry(row.class_id).or_default().push(row.level);
474 }
475
476 for (class_id, mut levels) in by_class {
477 levels.sort();
478 let mut dedup = levels.clone();
479 dedup.dedup();
480 if dedup.len() != levels.len() {
481 panic!("ClassLevels for class {class_id}: duplicate level entries");
482 }
483 for (i, level) in levels.iter().enumerate() {
484 let expected = (i as u64) + 1;
485 if *level != expected {
486 panic!(
487 "ClassLevels for class {class_id}: expected contiguous levels starting at 1, got {level} at position {expected}"
488 );
489 }
490 }
491 }
492 }
493
494 pub fn class_level(&self, class_id: class::ClassId, level: u64) -> Option<&class::ClassLevels> {
495 self.class_levels
496 .iter()
497 .find(|row| row.class_id == class_id && row.level == level)
498 }
499
500 fn validate_progress_pass_config(progress_pass: &essences::progress_pass::ProgressPassConfig) {
501 use std::collections::HashSet;
502
503 let mut tier_values: HashSet<u32> = HashSet::new();
504 for tier in &progress_pass.tiers {
505 if tier.quest_template_ids.is_empty() {
506 panic!(
507 "ProgressPassConfig: tier {} has no quest_template_ids",
508 tier.tier
509 );
510 }
511 if !tier_values.insert(tier.tier) {
512 panic!("ProgressPassConfig: duplicate tier value {}", tier.tier);
513 }
514 }
515 }
516
517 fn validate_progress_pass_references(&self) {
518 let pp = &self.progress_pass;
519
520 if pp.tiers.is_empty() {
521 return;
522 }
523
524 if !self
525 .offers_templates
526 .iter()
527 .any(|o| o.id == pp.premium_offer_template_id)
528 {
529 panic!(
530 "ProgressPassConfig: premium_offer_template_id {} does not match any offer template",
531 pp.premium_offer_template_id
532 );
533 }
534
535 for tier in &pp.tiers {
536 for quest_id in &tier.quest_template_ids {
537 let Some(quest) = self.quests.iter().find(|q| q.id == *quest_id) else {
538 panic!(
539 "ProgressPassConfig: tier {} references unknown quest template {quest_id}",
540 tier.tier
541 );
542 };
543 if quest.quest_group_type != QuestGroupType::ProgressPass {
544 panic!(
545 "ProgressPassConfig: tier {} references quest {quest_id} whose group is {:?}, expected ProgressPass",
546 tier.tier, quest.quest_group_type
547 );
548 }
549 }
550
551 if !self
552 .bundles
553 .iter()
554 .any(|b| b.id == tier.free_reward_bundle_id)
555 {
556 panic!(
557 "ProgressPassConfig: tier {} free_reward_bundle_id {} does not match any bundle",
558 tier.tier, tier.free_reward_bundle_id
559 );
560 }
561 if !self
562 .bundles
563 .iter()
564 .any(|b| b.id == tier.paid_reward_bundle_id)
565 {
566 panic!(
567 "ProgressPassConfig: tier {} paid_reward_bundle_id {} does not match any bundle",
568 tier.tier, tier.paid_reward_bundle_id
569 );
570 }
571 }
572 }
573
574 fn validate_loop_task_chain(quests: &[QuestTemplate]) {
575 use std::collections::{HashMap, HashSet};
576
577 let loop_quests: Vec<&QuestTemplate> = quests
578 .iter()
579 .filter(|q| q.quest_group_type == QuestGroupType::LoopTask)
580 .collect();
581
582 if loop_quests.is_empty() {
583 return;
584 }
585
586 let all_quest_ids: HashSet<_> = quests.iter().map(|q| q.id).collect();
587 let loop_quest_ids: HashSet<_> = loop_quests.iter().map(|q| q.id).collect();
588
589 let mut code_to_quest: HashMap<&str, &QuestTemplate> = HashMap::new();
591 for q in &loop_quests {
592 if let Some(code) = &q.code
593 && let Some(existing) = code_to_quest.insert(code.as_str(), q)
594 {
595 panic!(
596 "LoopTask: duplicate code '{}' on quests {} and {}",
597 code, existing.id, q.id
598 );
599 }
600 }
601
602 for q in &loop_quests {
604 for next_id in &q.next_quest_ids {
605 if !all_quest_ids.contains(next_id) {
606 panic!(
607 "LoopTask: quest {} ('{}') has next_quest_id {} that does not exist",
608 q.id,
609 q.code.as_deref().unwrap_or("<no code>"),
610 next_id
611 );
612 }
613 }
614 }
615
616 let mut sequences: HashMap<String, Vec<(u32, &QuestTemplate)>> = HashMap::new();
619 for (code, quest) in &code_to_quest {
620 let parts: Vec<&str> = code.split('.').collect();
621 if parts.len() == 3
623 && parts[0] == "loop_task"
624 && let Ok(n) = parts[2].parse::<u32>()
625 {
626 sequences
627 .entry(parts[1].to_string())
628 .or_default()
629 .push((n, quest));
630 }
631 }
632
633 for (seq_type, mut entries) in sequences {
634 entries.sort_by_key(|(n, _)| *n);
635
636 if entries[0].0 != 1 {
638 panic!(
639 "LoopTask: sequence 'loop_task.{}' starts at {} instead of 1",
640 seq_type, entries[0].0
641 );
642 }
643
644 for (i, (n, _)) in entries.iter().enumerate() {
646 let expected = (i as u32) + 1;
647 if *n != expected {
648 panic!(
649 "LoopTask: sequence 'loop_task.{}' has gap — expected {} but found {}",
650 seq_type, expected, n
651 );
652 }
653 }
654
655 if seq_type != "loop" {
657 for window in entries.windows(2) {
658 let (n, quest) = window[0];
659 let (_, next_quest) = window[1];
660 if !quest.next_quest_ids.contains(&next_quest.id) {
661 panic!(
662 "LoopTask: 'loop_task.{}.{}' (quest {}) does not chain to 'loop_task.{}.{}' (quest {}) via next_quest_ids",
663 seq_type,
664 n,
665 quest.id,
666 seq_type,
667 n + 1,
668 next_quest.id
669 );
670 }
671 }
672 }
673 }
674
675 let mut reachable: HashSet<essences::quest::QuestId> = HashSet::new();
678
679 for q in code_to_quest.values() {
681 reachable.insert(q.id);
682 }
683
684 let mut stack: Vec<essences::quest::QuestId> = Vec::new();
686 for q in quests {
687 for next_id in &q.next_quest_ids {
688 if loop_quest_ids.contains(next_id) {
689 stack.push(*next_id);
690 }
691 }
692 }
693 while let Some(id) = stack.pop() {
694 if !reachable.insert(id) {
695 continue;
696 }
697 if let Some(q) = loop_quests.iter().find(|q| q.id == id) {
698 for next_id in &q.next_quest_ids {
699 if loop_quest_ids.contains(next_id) {
700 stack.push(*next_id);
701 }
702 }
703 }
704 }
705
706 for q in &loop_quests {
707 if !reachable.contains(&q.id) {
708 panic!(
709 "LoopTask: quest {} ('{}') is unreachable — no code and not in next_quest_ids chain from a starting quest",
710 q.id,
711 q.code.as_deref().unwrap_or("<no code>")
712 );
713 }
714 }
715 }
716
717 fn validate_afk_rewards(
718 afk_levels: &[AfkRewardsByLevel],
719 afk_settings: &AfkRewardsSettings,
720 currencies: &[Currency],
721 abilities: &[AbilityTemplate],
722 items: &[ItemTemplate],
723 bundles: &[BundleRaw],
724 ) {
725 use std::collections::HashSet;
726
727 let currency_ids: HashSet<_> = currencies.iter().map(|c| c.id).collect();
728 let ability_ids: HashSet<_> = abilities.iter().map(|a| a.id).collect();
729 let item_ids: HashSet<_> = items.iter().map(|i| i.id).collect();
730 let bundle_ids: HashSet<_> = bundles.iter().map(|b| b.id).collect();
731
732 if !bundle_ids.contains(&afk_settings.bundle_id) {
735 panic!(
736 "AfkRewardsSettings: bundle_id {} not found in bundles",
737 afk_settings.bundle_id
738 );
739 }
740
741 let chapter_levels: Vec<_> = afk_levels.iter().map(|r| r.chapter_level).collect();
742 let unique_levels: HashSet<_> = chapter_levels.iter().collect();
743 if chapter_levels.len() != unique_levels.len() {
744 panic!("AfkRewardsByLevel: Two or more afk reward levels have equal chapter_level");
745 }
746
747 for level_rewards in afk_levels {
748 for rate in &level_rewards.currency_rates {
749 if !currency_ids.contains(&rate.currency_id) {
750 panic!(
751 "AfkRewardsByLevel chapter_level={}: currency_id {} not found in currencies",
752 level_rewards.chapter_level, rate.currency_id
753 );
754 }
755 }
757
758 for bonus_weight in &level_rewards.bonus_weights {
763 match &bonus_weight.bonus_type {
764 AfkRewardBonusType::Currency(currency_id) => {
765 if !currency_ids.contains(currency_id) {
766 panic!(
767 "AfkRewardsByLevel chapter_level={}: bonus Currency id {} not found",
768 level_rewards.chapter_level, currency_id
769 );
770 }
771 }
772 AfkRewardBonusType::Ability(ability_id) => {
773 if !ability_ids.contains(ability_id) {
774 panic!(
775 "AfkRewardsByLevel chapter_level={}: bonus Ability id {} not found",
776 level_rewards.chapter_level, ability_id
777 );
778 }
779 }
780 AfkRewardBonusType::Item(item_id) => {
781 if !item_ids.contains(item_id) {
782 panic!(
783 "AfkRewardsByLevel chapter_level={}: bonus Item id {} not found",
784 level_rewards.chapter_level, item_id
785 );
786 }
787 }
788 }
789 }
790 }
791 }
792
793 fn validate_ability_gacha_settings(&self) {
794 use std::collections::HashSet;
795
796 let gacha = &self.game_settings.ability_gacha;
797 let currency_ids: HashSet<_> = self.currencies.iter().map(|currency| currency.id).collect();
798 let rarity_ids: HashSet<_> = self
799 .ability_rarities
800 .iter()
801 .map(|rarity| rarity.id)
802 .collect();
803 let class_rarity_ids: HashSet<_> = self
804 .classes
805 .iter()
806 .map(|class| class.ability_rarity_id)
807 .collect();
808
809 if gacha.wishlist_slots == 0 {
813 panic!("AbilityGachaSettings: wishlist_slots must be > 0");
814 }
815 if gacha.slot_level_costs.len() != gacha.slot_max_level.get() as usize {
816 panic!(
817 "AbilityGachaSettings: slot_level_costs length ({}) must match slot_max_level ({})",
818 gacha.slot_level_costs.len(),
819 gacha.slot_max_level.get()
820 );
821 }
822 if gacha.slot_level_bonus_levels.len() != gacha.slot_max_level.get() as usize + 1 {
823 panic!(
824 "AbilityGachaSettings: slot_level_bonus_levels length ({}) must be slot_max_level + 1 ({})",
825 gacha.slot_level_bonus_levels.len(),
826 gacha.slot_max_level.get() + 1
827 );
828 }
829 for bonus in &gacha.slot_level_bonus_levels {
831 if *bonus < 0 {
832 panic!("AbilityGachaSettings: slot_level_bonus_levels values must be >= 0");
833 }
834 }
835 if !currency_ids.contains(&gacha.slot_upgrade_currency_id) {
836 panic!(
837 "AbilityGachaSettings: slot_upgrade_currency_id {} not found in currencies",
838 gacha.slot_upgrade_currency_id
839 );
840 }
841
842 let mut settings_by_level = self.ability_cases_settings.to_vec();
845 settings_by_level.sort_by_key(|level_settings| level_settings.level);
846
847 let boundary_count = settings_by_level
849 .iter()
850 .filter(|s| s.is_boundary_level)
851 .count();
852 if boundary_count > 1 {
853 panic!("AbilityCasesSettings: at most one boundary level is allowed");
854 }
855 if boundary_count == 1 && !settings_by_level.last().unwrap().is_boundary_level {
856 panic!("AbilityCasesSettings: the boundary level must be the last level");
857 }
858
859 let mut previous_level = 0;
860 let mut previous_opens = -1;
861 for (idx, level_settings) in settings_by_level.iter().enumerate() {
862 if level_settings.level <= previous_level {
863 panic!("AbilityCasesSettings: levels must be unique and strictly increasing");
864 }
865 if level_settings.opens_to_upgrade < 0 {
866 panic!(
867 "AbilityCasesSettings level {}: opens_to_upgrade must be >= 0",
868 level_settings.level
869 );
870 }
871 if level_settings.opens_to_upgrade < previous_opens {
872 panic!(
873 "AbilityCasesSettings level {}: opens_to_upgrade must be non-decreasing",
874 level_settings.level
875 );
876 }
877 previous_level = level_settings.level;
878 previous_opens = level_settings.opens_to_upgrade;
879
880 if level_settings.is_boundary_level {
882 continue;
883 }
884
885 let mut previous_checkpoint_opens = -1;
886 for checkpoint in &level_settings.checkpoints {
887 if checkpoint.required_opens <= 0 {
888 panic!(
889 "AbilityCasesSettings level {}: checkpoint required_opens must be > 0",
890 level_settings.level
891 );
892 }
893 if checkpoint.required_opens <= previous_checkpoint_opens {
894 panic!(
895 "AbilityCasesSettings level {}: checkpoint required_opens must be strictly increasing",
896 level_settings.level
897 );
898 }
899 previous_checkpoint_opens = checkpoint.required_opens;
900
901 if let Some(next) = settings_by_level.get(idx + 1) {
905 let level_range = next.opens_to_upgrade - level_settings.opens_to_upgrade;
906 if checkpoint.required_opens > level_range {
907 panic!(
908 "AbilityCasesSettings level {}: checkpoint required_opens ({}) exceeds level range ({})",
909 level_settings.level, checkpoint.required_opens, level_range
910 );
911 }
912 }
913
914 for currency_reward in &checkpoint.currency_rewards {
915 if currency_reward.amount <= 0 {
916 panic!(
917 "AbilityCasesSettings level {}: checkpoint currency reward must be > 0",
918 level_settings.level
919 );
920 }
921 if !currency_ids.contains(¤cy_reward.currency_id) {
922 panic!(
923 "AbilityCasesSettings level {}: checkpoint currency {} not found",
924 level_settings.level, currency_reward.currency_id
925 );
926 }
927 }
928
929 for ability_reward in &checkpoint.ability_rewards {
930 if ability_reward.amount < 0 {
931 panic!(
932 "AbilityCasesSettings level {}: checkpoint ability reward amount must be >= 0",
933 level_settings.level
934 );
935 }
936 if ability_reward.amount > 0
937 && !rarity_ids.contains(&ability_reward.min_rarity_id)
938 {
939 panic!(
940 "AbilityCasesSettings level {}: checkpoint ability reward min_rarity_id {} not found",
941 level_settings.level, ability_reward.min_rarity_id
942 );
943 }
944 }
945 }
946
947 for rarity_id in &level_settings.allowed_wishlist_rarity_ids {
948 if !rarity_ids.contains(rarity_id) {
949 panic!(
950 "AbilityCasesSettings level {}: allowed wishlist rarity {} not found",
951 level_settings.level, rarity_id
952 );
953 }
954 if class_rarity_ids.contains(rarity_id) {
955 panic!(
956 "AbilityCasesSettings level {}: class rarity {} cannot be in wishlist",
957 level_settings.level, rarity_id
958 );
959 }
960 }
961
962 for evolve_rule in &level_settings.evolve_rules {
963 if evolve_rule.small_roll_tries < 0 {
964 panic!(
965 "AbilityCasesSettings level {}: evolve small_roll_tries must be >= 0",
966 level_settings.level
967 );
968 }
969 if evolve_rule.big_roll_tries < 0 {
970 panic!(
971 "AbilityCasesSettings level {}: evolve big_roll_tries must be >= 0",
972 level_settings.level
973 );
974 }
975 if evolve_rule.small_roll_tries == 0 && evolve_rule.big_roll_tries == 0 {
976 panic!(
977 "AbilityCasesSettings level {}: evolve rule must have at least one non-zero tries",
978 level_settings.level
979 );
980 }
981 if !rarity_ids.contains(&evolve_rule.from_rarity_id) {
982 panic!(
983 "AbilityCasesSettings level {}: evolve from rarity {} not found",
984 level_settings.level, evolve_rule.from_rarity_id
985 );
986 }
987 let mut sum_weight = 0.0;
989 for rarity_weight in &evolve_rule.to_rarity_weights {
990 if !rarity_ids.contains(&rarity_weight.rarity_id) {
991 panic!(
992 "AbilityCasesSettings level {}: evolve to rarity {} not found",
993 level_settings.level, rarity_weight.rarity_id
994 );
995 }
996 sum_weight += rarity_weight.weight.get();
997 }
998 if sum_weight > 1.0 + f64::EPSILON {
999 panic!(
1000 "AbilityCasesSettings level {}: evolve to_rarity_weights sum must be <= 1.0, got {}",
1001 level_settings.level, sum_weight
1002 );
1003 }
1004 }
1005
1006 for currency_reward in &level_settings.big_roll_currency_rewards {
1007 if currency_reward.amount <= 0 {
1008 panic!(
1009 "AbilityCasesSettings level {}: big-roll currency reward amount must be > 0",
1010 level_settings.level
1011 );
1012 }
1013 if !currency_ids.contains(¤cy_reward.currency_id) {
1014 panic!(
1015 "AbilityCasesSettings level {}: big-roll currency {} not found",
1016 level_settings.level, currency_reward.currency_id
1017 );
1018 }
1019 }
1020
1021 let big_roll_shards = &level_settings.big_roll_shards;
1022 let shards_total: u64 = big_roll_shards.iter().map(|s| s.count as u64).sum();
1023 let expected_total =
1024 gacha.big_roll_cost.get() as u64 + gacha.big_roll_bonus_drops as u64;
1025 if shards_total != expected_total {
1026 panic!(
1027 "AbilityCasesSettings level {}: big_roll_shards total ({}) must equal big_roll_cost + bonus_drops ({})",
1028 level_settings.level, shards_total, expected_total
1029 );
1030 }
1031 for shard in big_roll_shards {
1032 if shard.count == 0 {
1033 panic!(
1034 "AbilityCasesSettings level {}: big_roll_shards count must be > 0",
1035 level_settings.level
1036 );
1037 }
1038 if !rarity_ids.contains(&shard.min_rarity_id) {
1039 panic!(
1040 "AbilityCasesSettings level {}: big_roll_shards rarity {} not found",
1041 level_settings.level, shard.min_rarity_id
1042 );
1043 }
1044 }
1045 }
1046
1047 if settings_by_level[0].level != 1 || settings_by_level[0].opens_to_upgrade != 0 {
1048 panic!("AbilityCasesSettings: level 1 with opens_to_upgrade = 0 is required");
1049 }
1050 }
1051
1052 fn validate_pet_config(&self) {
1053 use std::collections::HashSet;
1054
1055 let pet_template_ids: HashSet<_> = self.pet_templates.iter().map(|t| t.id).collect();
1056 if pet_template_ids.len() != self.pet_templates.len() {
1057 panic!("PetConfig: duplicate pet template ids");
1058 }
1059
1060 let pet_rarity_ids: HashSet<_> = self.pet_rarities.iter().map(|r| r.id).collect();
1061 if pet_rarity_ids.len() != self.pet_rarities.len() {
1062 panic!("PetConfig: duplicate pet rarity ids");
1063 }
1064
1065 let ability_ids: HashSet<_> = self.abilities.iter().map(|a| a.id).collect();
1066 let attribute_ids: HashSet<_> = self.attributes.iter().map(|a| a.id).collect();
1067
1068 for template in &self.pet_templates {
1069 if let Some(ability_id) = template.active_ability_id
1070 && !ability_ids.contains(&ability_id)
1071 {
1072 panic!(
1073 "PetConfig: pet template {} has active_ability_id {} not found in abilities",
1074 template.id, ability_id
1075 );
1076 }
1077 if let Some(ability_id) = template.passive_ability_id
1078 && !ability_ids.contains(&ability_id)
1079 {
1080 panic!(
1081 "PetConfig: pet template {} has passive_ability_id {} not found in abilities",
1082 template.id, ability_id
1083 );
1084 }
1085 if !pet_rarity_ids.contains(&template.rarity_id) {
1086 panic!(
1087 "PetConfig: pet template {} has rarity_id {} not found in pet_rarities",
1088 template.id, template.rarity_id
1089 );
1090 }
1091 for stat in &template.stats {
1092 if !attribute_ids.contains(&stat.attribute_id) {
1093 panic!(
1094 "PetConfig: pet template {} has stat attribute_id {} not found in attributes",
1095 template.id, stat.attribute_id
1096 );
1097 }
1098 }
1099 }
1100
1101 for pet_level in &self.pet_levels {
1102 if !pet_rarity_ids.contains(&pet_level.rarity_id) {
1103 panic!(
1104 "PetConfig: pet level {} has rarity_id {} not found in pet_rarities",
1105 pet_level.level, pet_level.rarity_id
1106 );
1107 }
1108 if pet_level.required_shards < 0 {
1109 panic!(
1110 "PetConfig: pet level {} (rarity {}) required_shards must be >= 0",
1111 pet_level.level, pet_level.rarity_id
1112 );
1113 }
1114 }
1115 }
1116
1117 fn validate_pet_gacha_settings(&self) {
1121 use std::collections::HashSet;
1122
1123 let gacha = &self.game_settings.pet_gacha;
1124 let currency_ids: HashSet<_> = self.currencies.iter().map(|currency| currency.id).collect();
1125 let rarity_ids: HashSet<_> = self.pet_rarities.iter().map(|rarity| rarity.id).collect();
1126
1127 if gacha.wishlist_slots == 0 {
1131 panic!("PetGachaSettings: wishlist_slots must be > 0");
1132 }
1133 if !currency_ids.contains(&gacha.currency_id) {
1134 panic!(
1135 "PetGachaSettings: currency_id {} not found in currencies",
1136 gacha.currency_id
1137 );
1138 }
1139
1140 if self.pet_cases_settings.is_empty() {
1141 panic!("PetCasesSettings: must not be empty");
1142 }
1143
1144 let mut settings_by_level = self.pet_cases_settings.clone();
1145 settings_by_level.sort_by_key(|level_settings| level_settings.level);
1146
1147 let boundary_count = settings_by_level
1149 .iter()
1150 .filter(|s| s.is_boundary_level)
1151 .count();
1152 if boundary_count > 1 {
1153 panic!("PetCasesSettings: at most one boundary level is allowed");
1154 }
1155 if boundary_count == 1 && !settings_by_level.last().unwrap().is_boundary_level {
1156 panic!("PetCasesSettings: the boundary level must be the last level");
1157 }
1158
1159 let mut previous_level = 0;
1160 let mut previous_opens = -1;
1161 for (idx, level_settings) in settings_by_level.iter().enumerate() {
1162 if level_settings.level <= previous_level {
1163 panic!("PetCasesSettings: levels must be unique and strictly increasing");
1164 }
1165 if level_settings.opens_to_upgrade < 0 {
1166 panic!(
1167 "PetCasesSettings level {}: opens_to_upgrade must be >= 0",
1168 level_settings.level
1169 );
1170 }
1171 if level_settings.opens_to_upgrade < previous_opens {
1172 panic!(
1173 "PetCasesSettings level {}: opens_to_upgrade must be non-decreasing",
1174 level_settings.level
1175 );
1176 }
1177 previous_level = level_settings.level;
1178 previous_opens = level_settings.opens_to_upgrade;
1179
1180 if level_settings.is_boundary_level {
1182 continue;
1183 }
1184
1185 if level_settings.rarity_weights.is_empty() {
1187 panic!(
1188 "PetCasesSettings level {}: rarity_weights must not be empty",
1189 level_settings.level
1190 );
1191 }
1192 for rarity_weight in &level_settings.rarity_weights {
1194 if !rarity_ids.contains(&rarity_weight.rarity_id) {
1195 panic!(
1196 "PetCasesSettings level {}: rarity weight rarity {} not found",
1197 level_settings.level, rarity_weight.rarity_id
1198 );
1199 }
1200 }
1201
1202 let mut previous_checkpoint_opens = -1;
1203 for checkpoint in &level_settings.checkpoints {
1204 if checkpoint.required_opens <= 0 {
1205 panic!(
1206 "PetCasesSettings level {}: checkpoint required_opens must be > 0",
1207 level_settings.level
1208 );
1209 }
1210 if checkpoint.required_opens <= previous_checkpoint_opens {
1211 panic!(
1212 "PetCasesSettings level {}: checkpoint required_opens must be strictly increasing",
1213 level_settings.level
1214 );
1215 }
1216 previous_checkpoint_opens = checkpoint.required_opens;
1217
1218 if let Some(next) = settings_by_level.get(idx + 1) {
1222 let level_range = next.opens_to_upgrade - level_settings.opens_to_upgrade;
1223 if checkpoint.required_opens > level_range {
1224 panic!(
1225 "PetCasesSettings level {}: checkpoint required_opens ({}) exceeds level range ({})",
1226 level_settings.level, checkpoint.required_opens, level_range
1227 );
1228 }
1229 }
1230
1231 for currency_reward in &checkpoint.currency_rewards {
1232 if currency_reward.amount <= 0 {
1233 panic!(
1234 "PetCasesSettings level {}: checkpoint currency reward must be > 0",
1235 level_settings.level
1236 );
1237 }
1238 if !currency_ids.contains(¤cy_reward.currency_id) {
1239 panic!(
1240 "PetCasesSettings level {}: checkpoint currency {} not found",
1241 level_settings.level, currency_reward.currency_id
1242 );
1243 }
1244 }
1245
1246 for pet_reward in &checkpoint.pet_rewards {
1247 if pet_reward.amount < 0 {
1248 panic!(
1249 "PetCasesSettings level {}: checkpoint pet reward amount must be >= 0",
1250 level_settings.level
1251 );
1252 }
1253 if pet_reward.amount > 0 && !rarity_ids.contains(&pet_reward.min_rarity_id) {
1254 panic!(
1255 "PetCasesSettings level {}: checkpoint pet reward min_rarity_id {} not found",
1256 level_settings.level, pet_reward.min_rarity_id
1257 );
1258 }
1259 }
1260 }
1261
1262 for rarity_id in &level_settings.allowed_wishlist_rarity_ids {
1263 if !rarity_ids.contains(rarity_id) {
1264 panic!(
1265 "PetCasesSettings level {}: allowed wishlist rarity {} not found",
1266 level_settings.level, rarity_id
1267 );
1268 }
1269 }
1270
1271 for currency_reward in &level_settings.big_roll_currency_rewards {
1272 if currency_reward.amount <= 0 {
1273 panic!(
1274 "PetCasesSettings level {}: big-roll currency reward amount must be > 0",
1275 level_settings.level
1276 );
1277 }
1278 if !currency_ids.contains(¤cy_reward.currency_id) {
1279 panic!(
1280 "PetCasesSettings level {}: big-roll currency {} not found",
1281 level_settings.level, currency_reward.currency_id
1282 );
1283 }
1284 }
1285
1286 let big_roll_shards = &level_settings.big_roll_shards;
1287 let shards_total: u64 = big_roll_shards.iter().map(|s| s.count as u64).sum();
1288 let expected_total =
1289 gacha.big_roll_cost.get() as u64 + gacha.big_roll_bonus_drops as u64;
1290 if shards_total > expected_total {
1291 panic!(
1292 "PetCasesSettings level {}: big_roll_shards total ({}) must not exceed big_roll_cost + bonus_drops ({})",
1293 level_settings.level, shards_total, expected_total
1294 );
1295 }
1296 if shards_total != expected_total {
1297 tracing::warn!(
1302 "PetCasesSettings level {}: big_roll_shards total ({}) does not match big_roll_cost + bonus_drops ({})",
1303 level_settings.level,
1304 shards_total,
1305 expected_total
1306 );
1307 }
1308 for shard in big_roll_shards {
1309 if shard.count == 0 {
1310 panic!(
1311 "PetCasesSettings level {}: big_roll_shards count must be > 0",
1312 level_settings.level
1313 );
1314 }
1315 if !rarity_ids.contains(&shard.min_rarity_id) {
1316 panic!(
1317 "PetCasesSettings level {}: big_roll_shards rarity {} not found",
1318 level_settings.level, shard.min_rarity_id
1319 );
1320 }
1321 }
1322 }
1323
1324 if settings_by_level[0].level != 1 || settings_by_level[0].opens_to_upgrade != 0 {
1325 panic!("PetCasesSettings: level 1 with opens_to_upgrade = 0 is required");
1326 }
1327 }
1328
1329 fn validate_statue_settings(&self) {
1330 use std::collections::HashSet;
1331
1332 let statue = &self.statue_settings;
1333 let currency_ids: HashSet<_> = self.currencies.iter().map(|c| c.id).collect();
1334 let attribute_ids: HashSet<_> = self.attributes.iter().map(|a| a.id).collect();
1335
1336 if !currency_ids.contains(&statue.currency_id) {
1337 panic!(
1338 "StatueSettings: currency_id {} not found in currencies",
1339 statue.currency_id
1340 );
1341 }
1342
1343 let grade_ids: HashSet<_> = self.statue_bonus_grades.iter().map(|g| g.id).collect();
1346 if grade_ids.len() != self.statue_bonus_grades.len() {
1347 panic!("statue_bonus_grades: duplicate grade ids");
1348 }
1349
1350 for bonus_type in &self.statue_bonus_type_configs {
1355 if !attribute_ids.contains(&bonus_type.attribute_id) {
1356 panic!(
1357 "statue_bonus_type_configs attribute_id {} not found in attributes",
1358 bonus_type.attribute_id
1359 );
1360 }
1361 for grade_value in &bonus_type.grade_values {
1363 if !grade_ids.contains(&grade_value.grade_id) {
1364 panic!(
1365 "statue_bonus_type_configs attribute {} references unknown grade_id {}",
1366 bonus_type.attribute_id, grade_value.grade_id
1367 );
1368 }
1369 }
1371 }
1372
1373 if self.statue_level_configs[0].level != 1
1375 || self.statue_level_configs[0].required_experience != 0
1376 {
1377 panic!("statue_level_configs must start with level=1 and required_experience=0");
1378 }
1379 let mut prev_level = 0;
1380 let mut prev_exp = -1;
1381 for level_config in &self.statue_level_configs {
1382 if level_config.level <= prev_level {
1383 panic!("statue_level_configs levels must be strictly increasing");
1384 }
1385 if level_config.required_experience <= prev_exp && level_config.level > 1 {
1386 panic!(
1387 "statue_level_configs level={} required_experience must be strictly increasing",
1388 level_config.level
1389 );
1390 }
1391 for weight in &level_config.grade_weights {
1395 if !grade_ids.contains(&weight.grade_id) {
1396 panic!(
1397 "statue_level_configs level={} grade_weights references unknown grade_id {}",
1398 level_config.level, weight.grade_id
1399 );
1400 }
1401 }
1403 prev_level = level_config.level;
1404 prev_exp = level_config.required_experience;
1405 }
1406 }
1407
1408 fn has_unique_ids(items: &[ItemTemplate]) -> bool {
1409 let mut ids = std::collections::HashSet::new();
1410 items.iter().all(|item| ids.insert(item.id))
1411 }
1412}
1413
1414#[derive(Debug, Clone, Copy)]
1415pub struct NextFightInfo {
1416 pub next_chapter_level: i64,
1417 pub next_fight_number: i64,
1418 pub next_fight_id: Option<essences::fighting::FightTemplateId>,
1419}
1420
1421#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Tsify)]
1422#[tsify(into_wasm_abi, from_wasm_abi)]
1423pub enum GetConfigResponse {
1424 Ok { config: Box<GameConfig> },
1425 Error { code: String, message: String },
1426}
1427
1428#[cfg(test)]
1429mod tests {
1430 use super::*;
1431 use essences::offers::OfferTemplateId;
1432 use essences::progress_pass::{ProgressPassConfig, ProgressPassTierTemplate};
1433 use uuid::uuid;
1434
1435 fn tier(tier: u32, quest_template_ids: Vec<uuid::Uuid>) -> ProgressPassTierTemplate {
1436 ProgressPassTierTemplate {
1437 tier,
1438 quest_template_ids,
1439 free_reward_bundle_id: uuid::Uuid::nil(),
1440 paid_reward_bundle_id: uuid::Uuid::nil(),
1441 }
1442 }
1443
1444 fn pp_config(tiers: Vec<ProgressPassTierTemplate>) -> ProgressPassConfig {
1445 ProgressPassConfig {
1446 tiers,
1447 premium_offer_template_id: OfferTemplateId::nil(),
1448 }
1449 }
1450
1451 #[test]
1452 fn validate_progress_pass_accepts_valid_config() {
1453 let config = pp_config(vec![
1454 tier(1, vec![uuid!("aa000000-0000-0000-0000-000000000001")]),
1455 tier(
1456 2,
1457 vec![
1458 uuid!("aa000000-0000-0000-0000-000000000001"),
1459 uuid!("bb000000-0000-0000-0000-000000000002"),
1460 ],
1461 ),
1462 tier(3, vec![uuid!("cc000000-0000-0000-0000-000000000003")]),
1463 ]);
1464 GameConfig::validate_progress_pass_config(&config);
1465 }
1466
1467 #[test]
1468 fn validate_progress_pass_accepts_empty_tiers() {
1469 let config = pp_config(vec![]);
1470 GameConfig::validate_progress_pass_config(&config);
1471 }
1472
1473 #[test]
1474 #[should_panic(expected = "ProgressPassConfig: tier 2 has no quest_template_ids")]
1475 fn validate_progress_pass_rejects_empty_quest_template_ids() {
1476 let config = pp_config(vec![
1477 tier(1, vec![uuid!("aa000000-0000-0000-0000-000000000001")]),
1478 tier(2, vec![]),
1479 ]);
1480 GameConfig::validate_progress_pass_config(&config);
1481 }
1482
1483 #[test]
1484 #[should_panic(expected = "ProgressPassConfig: duplicate tier value 1")]
1485 fn validate_progress_pass_rejects_duplicate_tier_numbers() {
1486 let config = pp_config(vec![
1487 tier(1, vec![uuid!("aa000000-0000-0000-0000-000000000001")]),
1488 tier(1, vec![uuid!("bb000000-0000-0000-0000-000000000002")]),
1489 ]);
1490 GameConfig::validate_progress_pass_config(&config);
1491 }
1492
1493 #[test]
1494 fn validate_pet_gacha_settings_accepts_test_config() {
1495 let config = crate::tests_game_config::generate_game_config_for_tests();
1496 config.validate_pet_gacha_settings();
1497 }
1498
1499 #[test]
1500 #[should_panic(expected = "PetCasesSettings: must not be empty")]
1501 fn validate_pet_gacha_settings_rejects_empty_cases_settings() {
1502 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1503 config.pet_cases_settings.clear();
1504 config.validate_pet_gacha_settings();
1505 }
1506
1507 #[test]
1508 #[should_panic(expected = "PetCasesSettings: level 1 with opens_to_upgrade = 0 is required")]
1509 fn validate_pet_gacha_settings_rejects_missing_level_one() {
1510 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1511 config.pet_cases_settings.remove(0);
1512 config.validate_pet_gacha_settings();
1513 }
1514
1515 #[test]
1516 #[should_panic(expected = "rarity weight rarity")]
1517 fn validate_pet_gacha_settings_rejects_unknown_rarity_in_weights() {
1518 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1519 config.pet_cases_settings[0].rarity_weights[0].rarity_id =
1520 uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
1521 config.validate_pet_gacha_settings();
1522 }
1523
1524 #[test]
1525 #[should_panic(expected = "rarity_weights must not be empty")]
1526 fn validate_pet_gacha_settings_rejects_empty_rarity_weights() {
1527 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1528 config.pet_cases_settings[0].rarity_weights.clear();
1529 config.validate_pet_gacha_settings();
1530 }
1531
1532 #[test]
1533 #[should_panic(expected = "must not exceed big_roll_cost + bonus_drops")]
1534 fn validate_pet_gacha_settings_rejects_shard_total_above_big_roll_drops() {
1535 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1536 config.pet_cases_settings[0].big_roll_shards[0].count = 200;
1537 config.validate_pet_gacha_settings();
1538 }
1539
1540 #[test]
1541 #[should_panic(expected = "big_roll_shards rarity")]
1542 fn validate_pet_gacha_settings_rejects_unknown_shard_rarity() {
1543 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1544 config.pet_cases_settings[0].big_roll_shards[0].min_rarity_id =
1545 uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
1546 config.validate_pet_gacha_settings();
1547 }
1548
1549 #[test]
1550 #[should_panic(expected = "checkpoint required_opens must be strictly increasing")]
1551 fn validate_pet_gacha_settings_rejects_non_increasing_checkpoints() {
1552 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1553 let checkpoint = config.pet_cases_settings[0].checkpoints[0].clone();
1554 config.pet_cases_settings[0].checkpoints.push(checkpoint);
1555 config.validate_pet_gacha_settings();
1556 }
1557
1558 #[test]
1559 #[should_panic(expected = "PetGachaSettings: wishlist_slots must be > 0")]
1560 fn validate_pet_gacha_settings_rejects_zero_wishlist_slots() {
1561 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1562 config.game_settings.pet_gacha.wishlist_slots = 0;
1563 config.validate_pet_gacha_settings();
1564 }
1565
1566 #[test]
1567 #[should_panic(expected = "not found in pet_rarities")]
1568 fn validate_pet_config_rejects_unknown_pet_level_rarity() {
1569 let mut config = crate::tests_game_config::generate_game_config_for_tests();
1570 config.pet_levels[0].rarity_id = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
1571 config.validate_pet_config();
1572 }
1573}