1use essences::{
2 currency::CurrencyId, fighting::FightTemplateId, items::AttributeId, offers::ShopTab,
3};
4use schema_loader::{
5 attribute_link_id_array_schema, attribute_link_id_schema, class_id_schema,
6 currency_link_id_array_schema, currency_link_id_schema, fight_template_link_id_schema,
7 vassal_task_id_schema,
8};
9use schemars::JsonSchema;
10
11use serde::{Deserialize, Serialize};
12use tsify_next::Tsify;
13
14use crate::validated_types::{PositiveI64, WeightMultiplier};
15
16#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
24pub struct MasteryShardBand {
25 #[schemars(
26 title = "По какое первое прохождение действует полоса",
27 description = "Включительно. Последняя полоса действует и дальше неё."
28 )]
29 pub up_to_clear: i64,
30
31 #[schemars(title = "Осколков за одно первое прохождение в этой полосе")]
32 pub shards: i64,
33}
34
35#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
38pub struct MasterySettings {
39 #[schemars(
40 title = "Валюта осколков мастерства",
41 schema_with = "currency_link_id_schema"
42 )]
43 pub shard_currency_id: CurrencyId,
44
45 #[schemars(
46 title = "Валюта кристаллов мастерства",
47 schema_with = "currency_link_id_schema"
48 )]
49 pub crystal_currency_id: CurrencyId,
50
51 #[schemars(
52 title = "Полосы выдачи осколков",
53 description = "По полосе на каждые десять первых прохождений; последняя действует и за своим краем."
54 )]
55 pub shard_bands: Vec<MasteryShardBand>,
56
57 #[schemars(
58 title = "Шанс кристалла за первое прохождение",
59 description = "Доля от 0 до 1. Повторные победы кристалл не роллят."
60 )]
61 pub crystal_chance: f64,
62
63 #[schemars(
64 title = "Гарантия кристалла после N неудач подряд",
65 description = "На этом по счёту неудачном ролле кристалл выдаётся гарантированно. 0 — гарантии нет."
66 )]
67 pub crystal_pity_clears: i64,
68}
69
70impl MasterySettings {
71 pub fn shards_for_clear(&self, clear: i64) -> i64 {
76 if clear <= 0 {
77 return 0;
78 }
79 let mut bands: Vec<&MasteryShardBand> = self.shard_bands.iter().collect();
80 bands.sort_by_key(|band| band.up_to_clear);
81 bands
82 .iter()
83 .find(|band| clear <= band.up_to_clear)
84 .or(bands.last())
85 .map_or(0, |band| band.shards.max(0))
86 }
87}
88
89#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
93pub struct PowerEstimatorSettings {
94 #[schemars(
95 title = "Эталонная частота триггера, раз в секунду",
96 description = "Частота, при которой авторские множители эффектов взяты как есть. Реальный триггер масштабирует их отношением своей частоты к этой."
97 )]
98 pub reference_trigger_proc_rate: f64,
99
100 #[schemars(
101 title = "Множитель Power пакета из одного триггера",
102 description = "Минимальный положительный вклад экипировки, у которой есть только триггер: шкала сама по себе ещё не даёт измеримых DPS/EHP, но прирост Power обязан быть виден."
103 )]
104 pub trigger_only_q: f64,
105
106 #[schemars(
107 title = "Ожидаемая заполненность моста",
108 description = "Доля ёмкости моста, доставляемая за фазу, пока нет trace. Входит в множитель Power моста."
109 )]
110 pub bridge_expected_fill: f64,
111}
112
113#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
114pub struct GameSettings {
115 #[schemars(title = "Настройки оценки Power")]
116 pub power_estimator: PowerEstimatorSettings,
117
118 #[schemars(title = "Мастерство класса: доход за первые прохождения боссов")]
119 pub mastery: MasterySettings,
120
121 #[schemars(
122 title = "Обязательные атрибуты предметов",
123 description = "Атрибуты, которые обязательно будут на предметах их сундука",
124 schema_with = "attribute_link_id_array_schema"
125 )]
126 pub required_attributes: Vec<AttributeId>,
127
128 #[schemars(
129 title = "Id вассальной таски брыкания",
130 schema_with = "vassal_task_id_schema"
131 )]
132 pub resist_task_id: uuid::Uuid,
133
134 #[schemars(title = "Id аттрибута хп", schema_with = "attribute_link_id_schema")]
135 pub hp_attribute_id: uuid::Uuid,
136
137 #[schemars(
138 title = "Id тайм скип билетика",
139 schema_with = "currency_link_id_schema"
140 )]
141 pub time_skip_ticket_currency_id: CurrencyId,
142
143 #[schemars(title = "Сколько секунд апгрейда сундука скипает тайм скип билетик")]
144 pub chest_upgrade_skip_ticket_skips_sec: u64,
145
146 #[schemars(
147 title = "Id валюты для скипа прокачки сундука",
148 schema_with = "currency_link_id_schema"
149 )]
150 pub chest_upgrade_skip_currency_id: CurrencyId,
151
152 #[schemars(title = "Сколько секунд апгрейда сундука скипает одна единица валюты")]
153 pub chest_upgrade_skip_currency_skips_sec: u64,
154
155 #[schemars(title = "Настройки гачи способностей")]
161 pub ability_gacha: AbilityGachaSettings,
162
163 #[schemars(
164 title = "Id валюты для открытия сундука предметов",
165 schema_with = "currency_link_id_schema"
166 )]
167 pub item_case_currency_id: CurrencyId,
168
169 #[schemars(
170 title = "Id изначального класса персонажа",
171 schema_with = "class_id_schema"
172 )]
173 pub initial_class_id: uuid::Uuid,
174
175 #[schemars(title = "Длительность хранения сообщения в часах")]
176 pub chat_message_ttl_secs: u64,
177
178 #[schemars(
179 title = "Базовое значение атрибута speed",
180 description = "Значение атрибута speed на entity, при котором кулдауны идут 1:1 (\"100%\"). При speed выше базового кулдауны идут быстрее, при меньшем — медленнее."
181 )]
182 pub baseline_speed: u64,
183
184 #[schemars(
185 title = "Id валют для отрисовки на главном экране",
186 schema_with = "currency_link_id_array_schema"
187 )]
188 pub currency_ids_to_show: Vec<CurrencyId>,
189
190 #[schemars(title = "Настройки гачи петов")]
191 pub pet_gacha: PetGachaSettings,
192
193 #[schemars(title = "Длительность паузы между открытиями сундука")]
194 pub auto_chest_pause_duration_ticks: u64,
195
196 #[schemars(title = "Добавка к паузе автосундука за каждый шар выданной пачки")]
202 pub auto_chest_per_ball_pause_ticks: u64,
203
204 #[schemars(
205 title = "Id битвы для арены",
206 schema_with = "fight_template_link_id_schema"
207 )]
208 pub arena_fight_template_id: FightTemplateId,
209
210 #[schemars(
211 title = "Id битвы для вассального pvp",
212 schema_with = "fight_template_link_id_schema"
213 )]
214 pub vassal_fight_id: FightTemplateId,
215
216 #[schemars(title = "Как часто срабатывает эвент fight_progress_tick")]
217 pub fight_progress_tick: u64,
218
219 #[schemars(title = "Частота обновления мощи персонажа в базе")]
220 pub db_power_update_frequency: u64,
221
222 #[schemars(title = "Юзернейм подставляющийся игроку удалившему аккаунт")]
223 pub deleted_user_username: String,
224
225 #[schemars(
226 title = "Id валюты бриллиантов",
227 schema_with = "currency_link_id_schema"
228 )]
229 pub diamond_currency_id: CurrencyId,
230
231 #[schemars(title = "Минимальный интервал между сменами username (секунды)")]
232 pub username_change_cooldown_sec: u64,
233
234 #[schemars(title = "Минимальная длина username")]
235 pub username_min_length: usize,
236
237 #[schemars(title = "Максимальная длина username")]
238 pub username_max_length: usize,
239
240 #[schemars(title = "До какого числа обновлять кол-во ключей у пользователя")]
241 pub dungeons_keys_reset_amount: i64,
242
243 #[schemars(title = "Настройки ежедневного ресета валют")]
244 pub daily_currency_resets: Vec<DailyCurrencyReset>,
245
246 #[schemars(title = "Вкладка магазина, открываемая по умолчанию")]
247 pub default_shop_tab: ShopTab,
248
249 #[schemars(
250 title = "Глава, на которой показывается окно \"оцените нас\"",
251 description = "Если задано, сервер эмитит событие ShowRateUs, когда игрок впервые достигает этой главы. None отключает автоматический показ."
252 )]
253 pub rate_us_chapter: Option<i64>,
254
255 #[serde(default)]
256 #[schemars(
257 title = "Рост награды босса за главу",
258 description = "Множитель валюты, выпадающей с босса главы, растёт как boss_reward_chapter_growth^(глава) — глубже глава, больше падает с босса (\"пушить выгодно\"). Масштабируются только боссы (is_boss) в кампанийных боях; треш и не-кампания не затронуты. None или <=1.0 отключает (множитель 1.0)."
259 )]
260 pub boss_reward_chapter_growth: Option<f64>,
261
262 #[serde(default)]
263 #[schemars(
264 title = "Потолок множителя награды босса",
265 description = "Верхняя граница множителя из boss_reward_chapter_growth (защищает экономику/overflow на глубоких главах). None = без доп. потолка (но множитель всё равно ограничен здравым пределом в коде)."
266 )]
267 pub boss_reward_max_multiplier: Option<f64>,
268
269 #[serde(default)]
270 #[schemars(
271 title = "Экспонента цены продажи предмета",
272 description = "Показатель степени в формуле цены продажи floor(eff^exp · coef). Меньше 1 — доход от продаж растёт сублинейно (голд-фаусет уплощается, чтобы геометрический сток апгрейда сундука связывал прогресс). None = зашитая константа (0.45). Env OVERLORD_BAL_SELL_EXP (сим-свип) перекрывает это значение."
273 )]
274 pub sell_price_exp: Option<f64>,
275
276 #[serde(default)]
277 #[schemars(
278 title = "Коэффициент цены продажи предмета",
279 description = "Коэффициент в формуле цены продажи floor(eff^exp · coef); якорит цену на низких eff (ранняя игра). None = зашитая константа (87). Env OVERLORD_BAL_SELL_COEF (сим-свип) перекрывает это значение."
280 )]
281 pub sell_price_coef: Option<f64>,
282
283 #[serde(default = "default_default_loop_task_behavior")]
290 #[schemars(skip)]
291 pub default_loop_task_behavior: String,
292}
293
294fn default_default_loop_task_behavior() -> String {
296 "default_loop_task_const".to_string()
297}
298
299#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
300pub struct AbilityGachaSettings {
301 #[schemars(
302 title = "Id валюты для открытия гачи скиллов",
303 schema_with = "currency_link_id_schema"
304 )]
305 pub currency_id: CurrencyId,
306
307 #[schemars(title = "Цена 1 крутки в бриллиантах")]
308 pub roll_price_in_diamonds: PositiveI64,
309
310 #[schemars(title = "Стоимость маленькой крутки")]
313 pub small_roll_cost: PositiveI64,
314
315 #[schemars(title = "Стоимость большой крутки")]
317 pub big_roll_cost: PositiveI64,
318
319 #[schemars(title = "Бонусные дропы в большой крутке")]
321 pub big_roll_bonus_drops: u8,
322
323 #[schemars(title = "Количество слотов вишлиста")]
325 pub wishlist_slots: u8,
326
327 #[schemars(title = "Множитель веса вишлиста")]
330 pub wishlist_weight_multiplier: WeightMultiplier,
331
332 #[schemars(
334 title = "Валюта прокачки слотов способностей",
335 schema_with = "currency_link_id_schema"
336 )]
337 pub slot_upgrade_currency_id: CurrencyId,
338
339 #[schemars(title = "Максимальный уровень слота способности")]
341 pub slot_max_level: PositiveI64,
342
343 #[schemars(title = "Стоимости уровней слотов способностей")]
346 pub slot_level_costs: Vec<PositiveI64>,
347
348 #[schemars(title = "Бонус уровня способности от уровня слота")]
351 pub slot_level_bonus_levels: Vec<i64>,
352}
353
354#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
355pub struct PetGachaSettings {
356 #[schemars(
357 title = "Id валюты для открытия гачи петов",
358 schema_with = "currency_link_id_schema"
359 )]
360 pub currency_id: CurrencyId,
361
362 #[schemars(title = "Цена 1 крутки в бриллиантах")]
363 pub roll_price_in_diamonds: PositiveI64,
364
365 #[schemars(title = "Стоимость маленькой крутки")]
368 pub small_roll_cost: PositiveI64,
369
370 #[schemars(title = "Стоимость большой крутки")]
372 pub big_roll_cost: PositiveI64,
373
374 #[schemars(title = "Бонусные дропы в большой крутке")]
376 pub big_roll_bonus_drops: u8,
377
378 #[schemars(title = "Количество слотов вишлиста")]
380 pub wishlist_slots: u8,
381
382 #[schemars(title = "Множитель веса вишлиста")]
385 pub wishlist_weight_multiplier: WeightMultiplier,
386
387 #[schemars(
394 title = "Пити первой копии пета, роллов",
395 description = "После стольких роллов подряд без новой копии следующий ролл гарантирует ещё не полученного пета. 0 — выключено."
396 )]
397 pub first_copy_pity_pulls: i64,
398}
399
400#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
401pub struct DailyCurrencyReset {
402 #[schemars(
403 title = "ID валюты для ресета",
404 schema_with = "currency_link_id_schema"
405 )]
406 pub currency_id: CurrencyId,
407
408 #[schemars(title = "Значение, до которого ресетить валюту")]
409 pub reset_amount: i64,
410}
411
412impl GameSettings {
413 pub fn get_daily_currency_reset_amount(&self, currency_id: CurrencyId) -> anyhow::Result<i64> {
414 self.daily_currency_resets
415 .iter()
416 .find(|reset| reset.currency_id == currency_id)
417 .map(|reset| reset.reset_amount)
418 .ok_or_else(|| {
419 anyhow::anyhow!(
420 "Currency {} not found in daily_currency_resets",
421 currency_id
422 )
423 })
424 }
425}