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::quest::{QuestGroupType, QuestTemplate, QuestsProgressionSettings};
20use essences::ratings::RatingSettings;
21use essences::referrals::ReferralLevelInfo;
22use essences::skins::{ConfigSkin, SkinsSettings};
23use essences::vassals::VassalTaskTemplate;
24use schema_loader::script_schema;
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, Debug, Serialize, Deserialize, JsonSchema, Tsify)]
49pub struct ScriptModule {
50 #[schemars(title = "Название")]
51 pub name: String,
52 #[schemars(title = "Скрипт", schema_with = "script_schema")]
53 pub script: String,
54}
55
56#[derive(Clone, Serialize, Deserialize, JsonSchema, Tsify)]
57#[tsify(into_wasm_abi)]
58pub struct GameConfig {
59 #[schemars(title = "Скриптовые библиотеки")]
60 pub script_modules: Vec<ScriptModule>,
61
62 #[schemars(title = "Атрибуты")]
63 pub attributes: Vec<Attribute>,
64
65 #[schemars(title = "Настройки сундука-гачи-айтемов")]
66 pub item_cases_settings: Vec<ItemCasesSettingsByLevel>,
67
68 #[schemars(
69 title = "Настройки игры",
70 description = "Отдельные значения, которые используются в игре"
71 )]
72 pub game_settings: GameSettings,
73
74 #[schemars(
75 title = "Настройки действий за рекламу",
76 description = "Все настройки действий, связанные с рекламой"
77 )]
78 pub ads_settings: AdsSettings,
79
80 #[schemars(title = "Предметы")]
81 pub items: Vec<ItemTemplate>,
82
83 #[schemars(title = "Редкости предметов")]
84 pub item_rarities: Vec<ItemRarity>,
85
86 #[schemars(title = "Скины")]
87 pub skins: Vec<ConfigSkin>,
88
89 #[schemars(title = "Настройки скинов для кастомизации")]
90 pub skins_settings: SkinsSettings,
91
92 #[schemars(title = "Эффекты")]
93 pub effects: Vec<Effect>,
94
95 #[schemars(title = "Способности")]
96 pub abilities: Vec<AbilityTemplate>,
97
98 #[schemars(title = "Настройки сундука-гачи-скилов")]
99 pub ability_cases_settings: NonEmptyVec<AbilityCasesSettingsByLevel>,
100
101 #[schemars(title = "Редкости способностей")]
102 pub ability_rarities: Vec<AbilityRarity>,
103
104 #[schemars(title = "Уровни способностей")]
105 pub ability_levels: Vec<AbilityLevel>,
106
107 #[schemars(title = "Настройки боя")]
108 pub fight_settings: FightSettings,
109
110 #[schemars(title = "Враги")]
111 pub entities: Vec<EntityTemplate>,
112
113 #[schemars(title = "Шаблоны данжей")]
114 pub dungeon_templates: Vec<DungeonTemplate>,
115
116 #[schemars(title = "Шаблоны боев")]
117 pub fight_templates: Vec<FightTemplate>,
118
119 #[schemars(title = "Главы кампании")]
120 pub chapters: Vec<Chapter>,
121
122 #[schemars(title = "Уровни игрока")]
123 pub character_levels: Vec<CharacterLevel>,
124
125 #[schemars(title = "Уровни слотов способностей")]
126 pub ability_slots_levels: Vec<AbilitySlotsLevel>,
127
128 #[schemars(title = "Реферальные уровни игрока")]
129 pub patron_levels: Vec<ReferralLevelInfo>,
130
131 #[schemars(title = "Квесты")]
132 pub quests: Vec<QuestTemplate>,
133
134 #[schemars(title = "Настройки прогрессии квестов")]
135 pub quests_progression_settings: QuestsProgressionSettings,
136
137 #[schemars(title = "Настройки вассалов")]
138 pub vassals_settings: VassalsSettings,
139
140 #[schemars(title = "Поручения")]
141 pub vassal_tasks: Vec<VassalTaskTemplate>,
142
143 #[schemars(title = "Настройки PvP")]
144 pub pvp_settings: PvpSettings,
145
146 #[schemars(title = "Подарки")]
147 pub gifts: Vec<GiftTemplate>,
148
149 #[schemars(title = "Шаблоны писем")]
150 pub mail_templates: Vec<MailTemplate>,
151
152 #[schemars(title = "Валюты")]
153 pub currencies: Vec<Currency>,
154
155 #[schemars(title = "Уровни инвентаря")]
156 pub inventory_levels: Vec<InventoryLevel>,
157
158 #[schemars(title = "Настройки арены")]
159 pub arena_settings: ArenaSettings,
160
161 #[schemars(title = "Настройки лиг")]
162 pub arena_leagues: Vec<ArenaLeague>,
163
164 #[schemars(title = "Настройки матчмейкинга")]
165 pub matchmaking_settings: MatchmakingSettings,
166
167 #[schemars(title = "Описания эвентов")]
168 pub event_descriptions: Vec<EventDescription>,
169
170 #[schemars(title = "Классы персонажа")]
171 pub classes: Vec<class::Class>,
172
173 #[schemars(title = "Список проджектайлов")]
174 pub projectiles: Vec<Projectile>,
175
176 #[schemars(title = "Список бандлов")]
177 pub bundles: Vec<BundleRaw>,
178
179 #[schemars(title = "Настройки пресетов способностей")]
180 pub ability_presets_settings: AbilityPresetsSettings,
181
182 #[schemars(title = "Настройки ботов")]
183 pub bots_settings: BotsSettings,
184
185 #[schemars(title = "Настройки репортов")]
186 pub reports_settings: ReportsSettings,
187
188 #[schemars(title = "Настройки афк наград")]
189 pub afk_rewards_settings: AfkRewardsSettings,
190
191 #[schemars(title = "Уровни афк наград")]
192 pub afk_rewards_levels: Vec<AfkRewardsByLevel>,
193
194 #[schemars(title = "Настройки генерации параметров пользователя")]
195 pub users_generating_settings: UsersGeneratingSettings,
196
197 #[schemars(title = "Шаги туториала")]
198 pub tutorial_steps: Vec<TutorialStep>,
199
200 #[schemars(title = "Настройки чатов")]
201 pub chats_settings: Vec<ChatSettings>,
202
203 #[schemars(title = "Шаблоны офферов")]
204 pub offers_templates: Vec<OfferTemplate>,
205
206 #[schemars(title = "Вкладки магазина")]
207 pub shop_tabs: Vec<ShopTabConfig>,
208
209 #[schemars(title = "Настройки рейтингов")]
210 pub ratings_settings: Vec<RatingSettings>,
211
212 #[schemars(title = "Набор скриптов для читов")]
213 pub cheat_scripts: Vec<CheatScript>,
214
215 #[schemars(
216 title = "Скрипты генерации тестового игрока",
217 description = "Используется только в режиме читов. Каждый скрипт в формате TestPlayerResult — задаёт предметы, способности, петов, уровень, класс и силу тестового игрока. Выбирается по индексу."
218 )]
219 pub test_player_scripts: Vec<TestPlayerScript>,
220
221 #[schemars(title = "Гейтинги")]
222 pub gatings: Gatings,
223
224 #[schemars(title = "Шаблоны петов")]
225 pub pet_templates: Vec<PetTemplate>,
226
227 #[schemars(title = "Редкости петов")]
228 pub pet_rarities: Vec<PetRarity>,
229
230 #[schemars(title = "Уровни петов")]
231 pub pet_levels: Vec<PetLevel>,
232
233 #[schemars(title = "Уровни слотов петов")]
234 pub pet_slots_levels: Vec<PetSlotsLevel>,
235
236 #[schemars(title = "Настройки сундука-гачи-петов")]
237 pub pet_cases_settings: Vec<PetCasesSettingsByLevel>,
238
239 #[schemars(title = "Настройки дерева талантов")]
240 pub talent_tree_settings: TalentTreeSettings,
241
242 #[schemars(title = "Таланты")]
243 pub talents: Vec<TalentTemplate>,
244
245 #[schemars(title = "Грейды бонусов статуи")]
246 pub statue_bonus_grades: NonEmptyVec<StatueBonusGrade>,
247
248 #[schemars(title = "Типы бонусов статуи (стат × грейд)")]
249 pub statue_bonus_type_configs: NonEmptyVec<StatueBonusTypeConfig>,
250
251 #[schemars(title = "Таблица уровней статуи")]
252 pub statue_level_configs: NonEmptyVec<StatueLevelConfig>,
253
254 #[schemars(title = "Настройки статуи героя")]
255 pub statue_settings: StatueSettings,
256
257 #[schemars(title = "Шаблоны баффов")]
258 pub buff_templates: Vec<BuffTemplate>,
259
260 #[schemars(title = "Варианты рекламных птиц")]
261 pub bird_variants: Vec<BirdVariant>,
262}
263
264impl std::fmt::Debug for GameConfig {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 write!(f, "GameConfig")
267 }
268}
269
270impl GameConfig {
271 pub fn clone_translate(&self, translator: &i18n::translator::Translator, locale: &str) -> Self {
272 let mut config = self.clone();
273 for ability in &mut config.abilities {
274 ability.name = translator.translate(&ability.name, locale);
275 ability.description = translator.translate(&ability.description, locale);
276 }
277
278 for attribute in &mut config.attributes {
279 attribute.name = translator.translate(&attribute.name, locale);
280 attribute.description = translator.translate(&attribute.description, locale);
281 if let Some(prefix) = &attribute.prefix {
282 attribute.prefix = Some(translator.translate(prefix, locale));
283 }
284 if let Some(suffix) = &attribute.suffix {
285 attribute.suffix = Some(translator.translate(suffix, locale));
286 }
287 }
288
289 for effect in &mut config.effects {
290 effect.name = translator.translate(&effect.name, locale);
291 }
292
293 for event in &mut config.event_descriptions {
294 event.description = translator.translate(&event.description, locale);
295 }
296
297 for item in &mut config.items {
298 item.name = translator.translate(&item.name, locale);
299 }
300
301 for item_rarity in &mut config.item_rarities {
302 item_rarity.name = translator.translate(&item_rarity.name, locale);
303 }
304
305 for skin in &mut config.skins {
306 skin.name = translator.translate(&skin.name, locale);
307 }
308
309 for ability_rarity in &mut config.ability_rarities {
310 ability_rarity.name = translator.translate(&ability_rarity.name, locale);
311 }
312
313 for quest in &mut config.quests {
314 quest.title = translator.translate(&quest.title, locale);
315 quest.description = translator.translate(&quest.description, locale);
316 }
317
318 for vassal_task in &mut config.vassal_tasks {
319 vassal_task.title = translator.translate(&vassal_task.title, locale);
320 }
321
322 for currency in &mut config.currencies {
323 currency.name = translator.translate(¤cy.name, locale);
324 currency.description = translator.translate(¤cy.description, locale);
325 }
326
327 for mail_template in &mut config.mail_templates {
328 mail_template.title = translator.translate(&mail_template.title, locale);
329 mail_template.message = translator.translate(&mail_template.message, locale);
330 }
331
332 for league in &mut config.arena_leagues {
333 league.name = translator.translate(&league.name, locale);
334 }
335
336 for fight in &mut config.fight_templates {
337 fight.title = translator.translate(&fight.title, locale);
338 }
339
340 for class in &mut config.classes {
341 class.name = translator.translate(&class.name, locale);
342 class.description = translator.translate(&class.description, locale);
343 }
344
345 for dungeon in &mut config.dungeon_templates {
346 dungeon.title = translator.translate(&dungeon.title, locale);
347 dungeon.description = translator.translate(&dungeon.description, locale);
348
349 for tip in &mut dungeon.tips {
350 tip.tip = translator.translate(&tip.tip, locale);
351 }
352 }
353
354 for step in &mut config.tutorial_steps {
355 step.text = translator.translate(&step.text, locale);
356 }
357
358 for offer in &mut config.offers_templates {
359 offer.title = translator.translate(&offer.title, locale);
360 if let Some(limit_buy_text) = &offer.limit_buy_text {
361 offer.limit_buy_text = Some(translator.translate(limit_buy_text, locale));
362 }
363 }
364
365 for shop_tab in &mut config.shop_tabs {
366 shop_tab.name = translator.translate(&shop_tab.name, locale);
367 }
368
369 for chapter in &mut config.chapters {
370 chapter.title = translator.translate(&chapter.title, locale);
371 }
372
373 for pet_template in &mut config.pet_templates {
374 pet_template.name = translator.translate(&pet_template.name, locale);
375 }
376
377 for pet_rarity in &mut config.pet_rarities {
378 pet_rarity.name = translator.translate(&pet_rarity.name, locale);
379 }
380
381 for talent in &mut config.talents {
382 talent.name = translator.translate(&talent.name, locale);
383 talent.description = translator.translate(&talent.description, locale);
384 }
385
386 config.ability_presets_settings.default_preset_name =
387 translator.translate(&config.ability_presets_settings.default_preset_name, locale);
388
389 for grade in &mut config.statue_bonus_grades {
390 grade.name = translator.translate(&grade.name, locale);
391 }
392
393 for buff in &mut config.buff_templates {
394 buff.title = translator.translate(&buff.title, locale);
395 buff.description = translator.translate(&buff.description, locale);
396 }
397
398 for skin in &mut config.skins {
399 if let Some(unlock_description) = &skin.unlock_description {
400 skin.unlock_description = Some(translator.translate(unlock_description, locale));
401 }
402 }
403
404 config
405 }
406
407 pub fn validate(&self) {
408 if !Self::has_unique_ids(&self.items) {
410 panic!("Not unique item ids in config");
411 }
412
413 self.validate_ability_gacha_settings();
414
415 Self::validate_afk_rewards(
416 &self.afk_rewards_levels,
417 &self.afk_rewards_settings,
418 &self.currencies,
419 &self.abilities,
420 &self.items,
421 &self.bundles,
422 );
423 Self::validate_loop_task_chain(&self.quests);
424 self.validate_pet_config();
425 self.validate_statue_settings();
426 }
427
428 fn validate_loop_task_chain(quests: &[QuestTemplate]) {
429 use std::collections::{HashMap, HashSet};
430
431 let loop_quests: Vec<&QuestTemplate> = quests
432 .iter()
433 .filter(|q| q.quest_group_type == QuestGroupType::LoopTask)
434 .collect();
435
436 if loop_quests.is_empty() {
437 return;
438 }
439
440 let all_quest_ids: HashSet<_> = quests.iter().map(|q| q.id).collect();
441 let loop_quest_ids: HashSet<_> = loop_quests.iter().map(|q| q.id).collect();
442
443 let mut code_to_quest: HashMap<&str, &QuestTemplate> = HashMap::new();
445 for q in &loop_quests {
446 if let Some(code) = &q.code
447 && let Some(existing) = code_to_quest.insert(code.as_str(), q)
448 {
449 panic!(
450 "LoopTask: duplicate code '{}' on quests {} and {}",
451 code, existing.id, q.id
452 );
453 }
454 }
455
456 for q in &loop_quests {
458 if q.additional_quests_script.is_none() {
459 panic!(
460 "LoopTask: quest {} ('{}') has no additional_quests_script — chain will break",
461 q.id,
462 q.code.as_deref().unwrap_or("<no code>")
463 );
464 }
465 }
466
467 for q in &loop_quests {
469 for next_id in &q.next_quest_ids {
470 if !all_quest_ids.contains(next_id) {
471 panic!(
472 "LoopTask: quest {} ('{}') has next_quest_id {} that does not exist",
473 q.id,
474 q.code.as_deref().unwrap_or("<no code>"),
475 next_id
476 );
477 }
478 }
479 }
480
481 let mut sequences: HashMap<String, Vec<(u32, &QuestTemplate)>> = HashMap::new();
484 for (code, quest) in &code_to_quest {
485 let parts: Vec<&str> = code.split('.').collect();
486 if parts.len() == 3
488 && parts[0] == "loop_task"
489 && let Ok(n) = parts[2].parse::<u32>()
490 {
491 sequences
492 .entry(parts[1].to_string())
493 .or_default()
494 .push((n, quest));
495 }
496 }
497
498 for (seq_type, mut entries) in sequences {
499 entries.sort_by_key(|(n, _)| *n);
500
501 if entries[0].0 != 1 {
503 panic!(
504 "LoopTask: sequence 'loop_task.{}' starts at {} instead of 1",
505 seq_type, entries[0].0
506 );
507 }
508
509 for (i, (n, _)) in entries.iter().enumerate() {
511 let expected = (i as u32) + 1;
512 if *n != expected {
513 panic!(
514 "LoopTask: sequence 'loop_task.{}' has gap — expected {} but found {}",
515 seq_type, expected, n
516 );
517 }
518 }
519
520 if seq_type != "loop" {
523 for window in entries.windows(2) {
524 let (n, quest) = window[0];
525 let (_, next_quest) = window[1];
526 if !quest.next_quest_ids.contains(&next_quest.id) {
527 panic!(
528 "LoopTask: 'loop_task.{}.{}' (quest {}) does not chain to 'loop_task.{}.{}' (quest {}) via next_quest_ids",
529 seq_type,
530 n,
531 quest.id,
532 seq_type,
533 n + 1,
534 next_quest.id
535 );
536 }
537 }
538 }
539 }
540
541 let mut reachable: HashSet<essences::quest::QuestId> = HashSet::new();
544
545 for q in code_to_quest.values() {
547 reachable.insert(q.id);
548 }
549
550 let mut stack: Vec<essences::quest::QuestId> = Vec::new();
552 for q in quests {
553 for next_id in &q.next_quest_ids {
554 if loop_quest_ids.contains(next_id) {
555 stack.push(*next_id);
556 }
557 }
558 }
559 while let Some(id) = stack.pop() {
560 if !reachable.insert(id) {
561 continue;
562 }
563 if let Some(q) = loop_quests.iter().find(|q| q.id == id) {
564 for next_id in &q.next_quest_ids {
565 if loop_quest_ids.contains(next_id) {
566 stack.push(*next_id);
567 }
568 }
569 }
570 }
571
572 for q in &loop_quests {
573 if !reachable.contains(&q.id) {
574 panic!(
575 "LoopTask: quest {} ('{}') is unreachable — no code and not in next_quest_ids chain from a starting quest",
576 q.id,
577 q.code.as_deref().unwrap_or("<no code>")
578 );
579 }
580 }
581 }
582
583 fn validate_afk_rewards(
584 afk_levels: &[AfkRewardsByLevel],
585 afk_settings: &AfkRewardsSettings,
586 currencies: &[Currency],
587 abilities: &[AbilityTemplate],
588 items: &[ItemTemplate],
589 bundles: &[BundleRaw],
590 ) {
591 use std::collections::HashSet;
592
593 let currency_ids: HashSet<_> = currencies.iter().map(|c| c.id).collect();
594 let ability_ids: HashSet<_> = abilities.iter().map(|a| a.id).collect();
595 let item_ids: HashSet<_> = items.iter().map(|i| i.id).collect();
596 let bundle_ids: HashSet<_> = bundles.iter().map(|b| b.id).collect();
597
598 if !bundle_ids.contains(&afk_settings.bundle_id) {
601 panic!(
602 "AfkRewardsSettings: bundle_id {} not found in bundles",
603 afk_settings.bundle_id
604 );
605 }
606
607 let chapter_levels: Vec<_> = afk_levels.iter().map(|r| r.chapter_level).collect();
608 let unique_levels: HashSet<_> = chapter_levels.iter().collect();
609 if chapter_levels.len() != unique_levels.len() {
610 panic!("AfkRewardsByLevel: Two or more afk reward levels have equal chapter_level");
611 }
612
613 for level_rewards in afk_levels {
614 for rate in &level_rewards.currency_rates {
615 if !currency_ids.contains(&rate.currency_id) {
616 panic!(
617 "AfkRewardsByLevel chapter_level={}: currency_id {} not found in currencies",
618 level_rewards.chapter_level, rate.currency_id
619 );
620 }
621 }
623
624 for bonus_weight in &level_rewards.bonus_weights {
629 match &bonus_weight.bonus_type {
630 AfkRewardBonusType::Currency(currency_id) => {
631 if !currency_ids.contains(currency_id) {
632 panic!(
633 "AfkRewardsByLevel chapter_level={}: bonus Currency id {} not found",
634 level_rewards.chapter_level, currency_id
635 );
636 }
637 }
638 AfkRewardBonusType::Ability(ability_id) => {
639 if !ability_ids.contains(ability_id) {
640 panic!(
641 "AfkRewardsByLevel chapter_level={}: bonus Ability id {} not found",
642 level_rewards.chapter_level, ability_id
643 );
644 }
645 }
646 AfkRewardBonusType::Item(item_id) => {
647 if !item_ids.contains(item_id) {
648 panic!(
649 "AfkRewardsByLevel chapter_level={}: bonus Item id {} not found",
650 level_rewards.chapter_level, item_id
651 );
652 }
653 }
654 }
655 }
656 }
657 }
658
659 fn validate_ability_gacha_settings(&self) {
660 use std::collections::HashSet;
661
662 let gacha = &self.game_settings.ability_gacha;
663 let currency_ids: HashSet<_> = self.currencies.iter().map(|currency| currency.id).collect();
664 let rarity_ids: HashSet<_> = self
665 .ability_rarities
666 .iter()
667 .map(|rarity| rarity.id)
668 .collect();
669 let class_rarity_ids: HashSet<_> = self
670 .classes
671 .iter()
672 .map(|class| class.ability_rarity_id)
673 .collect();
674
675 if gacha.wishlist_slots == 0 {
679 panic!("AbilityGachaSettings: wishlist_slots must be > 0");
680 }
681 if gacha.slot_level_costs.len() != gacha.slot_max_level.get() as usize {
682 panic!(
683 "AbilityGachaSettings: slot_level_costs length ({}) must match slot_max_level ({})",
684 gacha.slot_level_costs.len(),
685 gacha.slot_max_level.get()
686 );
687 }
688 if gacha.slot_level_bonus_levels.len() != gacha.slot_max_level.get() as usize + 1 {
689 panic!(
690 "AbilityGachaSettings: slot_level_bonus_levels length ({}) must be slot_max_level + 1 ({})",
691 gacha.slot_level_bonus_levels.len(),
692 gacha.slot_max_level.get() + 1
693 );
694 }
695 for bonus in &gacha.slot_level_bonus_levels {
697 if *bonus < 0 {
698 panic!("AbilityGachaSettings: slot_level_bonus_levels values must be >= 0");
699 }
700 }
701 if !currency_ids.contains(&gacha.slot_upgrade_currency_id) {
702 panic!(
703 "AbilityGachaSettings: slot_upgrade_currency_id {} not found in currencies",
704 gacha.slot_upgrade_currency_id
705 );
706 }
707
708 let mut settings_by_level = self.ability_cases_settings.to_vec();
711 settings_by_level.sort_by_key(|level_settings| level_settings.level);
712
713 let boundary_count = settings_by_level
715 .iter()
716 .filter(|s| s.is_boundary_level)
717 .count();
718 if boundary_count > 1 {
719 panic!("AbilityCasesSettings: at most one boundary level is allowed");
720 }
721 if boundary_count == 1 && !settings_by_level.last().unwrap().is_boundary_level {
722 panic!("AbilityCasesSettings: the boundary level must be the last level");
723 }
724
725 let mut previous_level = 0;
726 let mut previous_opens = -1;
727 for (idx, level_settings) in settings_by_level.iter().enumerate() {
728 if level_settings.level <= previous_level {
729 panic!("AbilityCasesSettings: levels must be unique and strictly increasing");
730 }
731 if level_settings.opens_to_upgrade < 0 {
732 panic!(
733 "AbilityCasesSettings level {}: opens_to_upgrade must be >= 0",
734 level_settings.level
735 );
736 }
737 if level_settings.opens_to_upgrade < previous_opens {
738 panic!(
739 "AbilityCasesSettings level {}: opens_to_upgrade must be non-decreasing",
740 level_settings.level
741 );
742 }
743 previous_level = level_settings.level;
744 previous_opens = level_settings.opens_to_upgrade;
745
746 if level_settings.is_boundary_level {
748 continue;
749 }
750
751 let mut previous_checkpoint_opens = -1;
752 for checkpoint in &level_settings.checkpoints {
753 if checkpoint.required_opens <= 0 {
754 panic!(
755 "AbilityCasesSettings level {}: checkpoint required_opens must be > 0",
756 level_settings.level
757 );
758 }
759 if checkpoint.required_opens <= previous_checkpoint_opens {
760 panic!(
761 "AbilityCasesSettings level {}: checkpoint required_opens must be strictly increasing",
762 level_settings.level
763 );
764 }
765 previous_checkpoint_opens = checkpoint.required_opens;
766
767 if let Some(next) = settings_by_level.get(idx + 1) {
771 let level_range = next.opens_to_upgrade - level_settings.opens_to_upgrade;
772 if checkpoint.required_opens > level_range {
773 panic!(
774 "AbilityCasesSettings level {}: checkpoint required_opens ({}) exceeds level range ({})",
775 level_settings.level, checkpoint.required_opens, level_range
776 );
777 }
778 }
779
780 for currency_reward in &checkpoint.currency_rewards {
781 if currency_reward.amount <= 0 {
782 panic!(
783 "AbilityCasesSettings level {}: checkpoint currency reward must be > 0",
784 level_settings.level
785 );
786 }
787 if !currency_ids.contains(¤cy_reward.currency_id) {
788 panic!(
789 "AbilityCasesSettings level {}: checkpoint currency {} not found",
790 level_settings.level, currency_reward.currency_id
791 );
792 }
793 }
794
795 for ability_reward in &checkpoint.ability_rewards {
796 if ability_reward.amount < 0 {
797 panic!(
798 "AbilityCasesSettings level {}: checkpoint ability reward amount must be >= 0",
799 level_settings.level
800 );
801 }
802 if ability_reward.amount > 0
803 && !rarity_ids.contains(&ability_reward.min_rarity_id)
804 {
805 panic!(
806 "AbilityCasesSettings level {}: checkpoint ability reward min_rarity_id {} not found",
807 level_settings.level, ability_reward.min_rarity_id
808 );
809 }
810 }
811 }
812
813 for rarity_id in &level_settings.allowed_wishlist_rarity_ids {
814 if !rarity_ids.contains(rarity_id) {
815 panic!(
816 "AbilityCasesSettings level {}: allowed wishlist rarity {} not found",
817 level_settings.level, rarity_id
818 );
819 }
820 if class_rarity_ids.contains(rarity_id) {
821 panic!(
822 "AbilityCasesSettings level {}: class rarity {} cannot be in wishlist",
823 level_settings.level, rarity_id
824 );
825 }
826 }
827
828 for evolve_rule in &level_settings.evolve_rules {
829 if evolve_rule.small_roll_tries < 0 {
830 panic!(
831 "AbilityCasesSettings level {}: evolve small_roll_tries must be >= 0",
832 level_settings.level
833 );
834 }
835 if evolve_rule.big_roll_tries < 0 {
836 panic!(
837 "AbilityCasesSettings level {}: evolve big_roll_tries must be >= 0",
838 level_settings.level
839 );
840 }
841 if evolve_rule.small_roll_tries == 0 && evolve_rule.big_roll_tries == 0 {
842 panic!(
843 "AbilityCasesSettings level {}: evolve rule must have at least one non-zero tries",
844 level_settings.level
845 );
846 }
847 if !rarity_ids.contains(&evolve_rule.from_rarity_id) {
848 panic!(
849 "AbilityCasesSettings level {}: evolve from rarity {} not found",
850 level_settings.level, evolve_rule.from_rarity_id
851 );
852 }
853 let mut sum_weight = 0.0;
855 for rarity_weight in &evolve_rule.to_rarity_weights {
856 if !rarity_ids.contains(&rarity_weight.rarity_id) {
857 panic!(
858 "AbilityCasesSettings level {}: evolve to rarity {} not found",
859 level_settings.level, rarity_weight.rarity_id
860 );
861 }
862 sum_weight += rarity_weight.weight.get();
863 }
864 if sum_weight > 1.0 + f64::EPSILON {
865 panic!(
866 "AbilityCasesSettings level {}: evolve to_rarity_weights sum must be <= 1.0, got {}",
867 level_settings.level, sum_weight
868 );
869 }
870 }
871
872 for currency_reward in &level_settings.big_roll_currency_rewards {
873 if currency_reward.amount <= 0 {
874 panic!(
875 "AbilityCasesSettings level {}: big-roll currency reward amount must be > 0",
876 level_settings.level
877 );
878 }
879 if !currency_ids.contains(¤cy_reward.currency_id) {
880 panic!(
881 "AbilityCasesSettings level {}: big-roll currency {} not found",
882 level_settings.level, currency_reward.currency_id
883 );
884 }
885 }
886
887 let big_roll_shards = &level_settings.big_roll_shards;
888 let shards_total: u64 = big_roll_shards.iter().map(|s| s.count as u64).sum();
889 let expected_total =
890 gacha.big_roll_cost.get() as u64 + gacha.big_roll_bonus_drops as u64;
891 if shards_total != expected_total {
892 panic!(
893 "AbilityCasesSettings level {}: big_roll_shards total ({}) must equal big_roll_cost + bonus_drops ({})",
894 level_settings.level, shards_total, expected_total
895 );
896 }
897 for shard in big_roll_shards {
898 if shard.count == 0 {
899 panic!(
900 "AbilityCasesSettings level {}: big_roll_shards count must be > 0",
901 level_settings.level
902 );
903 }
904 if !rarity_ids.contains(&shard.min_rarity_id) {
905 panic!(
906 "AbilityCasesSettings level {}: big_roll_shards rarity {} not found",
907 level_settings.level, shard.min_rarity_id
908 );
909 }
910 }
911 }
912
913 if settings_by_level[0].level != 1 || settings_by_level[0].opens_to_upgrade != 0 {
914 panic!("AbilityCasesSettings: level 1 with opens_to_upgrade = 0 is required");
915 }
916 }
917
918 fn validate_pet_config(&self) {
919 use std::collections::HashSet;
920
921 let pet_template_ids: HashSet<_> = self.pet_templates.iter().map(|t| t.id).collect();
922 if pet_template_ids.len() != self.pet_templates.len() {
923 panic!("PetConfig: duplicate pet template ids");
924 }
925
926 let pet_rarity_ids: HashSet<_> = self.pet_rarities.iter().map(|r| r.id).collect();
927 if pet_rarity_ids.len() != self.pet_rarities.len() {
928 panic!("PetConfig: duplicate pet rarity ids");
929 }
930
931 let ability_ids: HashSet<_> = self.abilities.iter().map(|a| a.id).collect();
932 let attribute_ids: HashSet<_> = self.attributes.iter().map(|a| a.id).collect();
933
934 for template in &self.pet_templates {
935 if let Some(ability_id) = template.active_ability_id
936 && !ability_ids.contains(&ability_id)
937 {
938 panic!(
939 "PetConfig: pet template {} has active_ability_id {} not found in abilities",
940 template.id, ability_id
941 );
942 }
943 if let Some(ability_id) = template.passive_ability_id
944 && !ability_ids.contains(&ability_id)
945 {
946 panic!(
947 "PetConfig: pet template {} has passive_ability_id {} not found in abilities",
948 template.id, ability_id
949 );
950 }
951 if !pet_rarity_ids.contains(&template.rarity_id) {
952 panic!(
953 "PetConfig: pet template {} has rarity_id {} not found in pet_rarities",
954 template.id, template.rarity_id
955 );
956 }
957 for stat in &template.stats {
958 if !attribute_ids.contains(&stat.attribute_id) {
959 panic!(
960 "PetConfig: pet template {} has stat attribute_id {} not found in attributes",
961 template.id, stat.attribute_id
962 );
963 }
964 }
965 }
966 }
967
968 fn validate_statue_settings(&self) {
969 use std::collections::HashSet;
970
971 let statue = &self.statue_settings;
972 let currency_ids: HashSet<_> = self.currencies.iter().map(|c| c.id).collect();
973 let attribute_ids: HashSet<_> = self.attributes.iter().map(|a| a.id).collect();
974
975 if !currency_ids.contains(&statue.currency_id) {
976 panic!(
977 "StatueSettings: currency_id {} not found in currencies",
978 statue.currency_id
979 );
980 }
981
982 let grade_ids: HashSet<_> = self.statue_bonus_grades.iter().map(|g| g.id).collect();
985 if grade_ids.len() != self.statue_bonus_grades.len() {
986 panic!("statue_bonus_grades: duplicate grade ids");
987 }
988
989 for bonus_type in &self.statue_bonus_type_configs {
994 if !attribute_ids.contains(&bonus_type.attribute_id) {
995 panic!(
996 "statue_bonus_type_configs attribute_id {} not found in attributes",
997 bonus_type.attribute_id
998 );
999 }
1000 for grade_value in &bonus_type.grade_values {
1002 if !grade_ids.contains(&grade_value.grade_id) {
1003 panic!(
1004 "statue_bonus_type_configs attribute {} references unknown grade_id {}",
1005 bonus_type.attribute_id, grade_value.grade_id
1006 );
1007 }
1008 }
1010 }
1011
1012 if self.statue_level_configs[0].level != 1
1014 || self.statue_level_configs[0].required_experience != 0
1015 {
1016 panic!("statue_level_configs must start with level=1 and required_experience=0");
1017 }
1018 let mut prev_level = 0;
1019 let mut prev_exp = -1;
1020 for level_config in &self.statue_level_configs {
1021 if level_config.level <= prev_level {
1022 panic!("statue_level_configs levels must be strictly increasing");
1023 }
1024 if level_config.required_experience <= prev_exp && level_config.level > 1 {
1025 panic!(
1026 "statue_level_configs level={} required_experience must be strictly increasing",
1027 level_config.level
1028 );
1029 }
1030 for weight in &level_config.grade_weights {
1034 if !grade_ids.contains(&weight.grade_id) {
1035 panic!(
1036 "statue_level_configs level={} grade_weights references unknown grade_id {}",
1037 level_config.level, weight.grade_id
1038 );
1039 }
1040 }
1042 prev_level = level_config.level;
1043 prev_exp = level_config.required_experience;
1044 }
1045 }
1046
1047 fn has_unique_ids(items: &[ItemTemplate]) -> bool {
1048 let mut ids = std::collections::HashSet::new();
1049 items.iter().all(|item| ids.insert(item.id))
1050 }
1051}
1052
1053#[derive(Debug, Clone, Copy)]
1054pub struct NextFightInfo {
1055 pub next_chapter_level: i64,
1056 pub next_fight_number: i64,
1057 pub next_fight_id: Option<essences::fighting::FightTemplateId>,
1058}
1059
1060#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Tsify)]
1061#[tsify(into_wasm_abi, from_wasm_abi)]
1062pub enum GetConfigResponse {
1063 Ok { config: Box<GameConfig> },
1064 Error { code: String, message: String },
1065}