configs/
game_settings.rs

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/// One band of the Mastery Shard schedule (BAL-035): every campaign-boss FIRST
17/// clear up to `up_to_clear` pays `shards`.
18///
19/// Bands rather than a formula because the schedule is authored against exact
20/// cumulative totals — `100` Shards by the tenth clear, `4,000` by the
21/// hundredth, `16,000` by state ch300 — and those totals are what the Mastery
22/// price ladder was priced against.
23#[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/// Mastery income (BAL-035): what a campaign-boss first clear pays toward the
36/// shared Class Level.
37#[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    /// Shards paid by the `clear`-th campaign-boss FIRST clear (1-based). The
72    /// last band runs on past its own edge, so a player deeper than the
73    /// authored schedule keeps earning at its final rate instead of dropping to
74    /// nothing.
75    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/// The three scalars the approximate `P_ui` estimator needs beyond the
90/// per-template coefficients (BAL-030). Everything else it reads is authored on
91/// the templates themselves, so a new law or stone brings its own Power row.
92#[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 = "Ключ значения для повторяющихся квестов в custom_values")]
156    // pub loop_tasks_custom_values_key: String,
157
158    // #[schemars(title = "Дефолтное значение для повторяющихся квестов в custom_values")]
159    // pub loop_tasks_custom_values_default_value: i64,
160    #[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    /// Клиент показывает шары пачки по одному, поэтому пауза перед следующим
197    /// автооткрытием растёт с размером выданной пачки:
198    /// `auto_chest_pause_duration_ticks + n × auto_chest_per_ball_pause_ticks`.
199    /// Без этой добавки сервер обгоняет анимацию и очередь шаров на клиенте
200    /// растёт без ограничения.
201    #[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    /// Name of the native `default_loop_task` fn used to recover the active loop
284    /// task when `active_loop_task_id` is set but the loop-task list rebuilt
285    /// Production defaults to `"default_loop_task_const"`; the test fixture
286    /// overrides it to `"test_default_loop_task"` (which returns the fixture's
287    /// own loop-task id `305300e2`). Same postcard-symmetry / schema-skip
288    /// reasoning as `item_price_script`.
289    #[serde(default = "default_default_loop_task_behavior")]
290    #[schemars(skip)]
291    pub default_loop_task_behavior: String,
292}
293
294/// Default selector for [`GameSettings::default_loop_task_behavior`].
295fn 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    /// Стоимость маленькой крутки в gacha-билетах.
311    /// Она же — количество дропов и очки прогресса (тикеты = поинты = дропы).
312    #[schemars(title = "Стоимость маленькой крутки")]
313    pub small_roll_cost: PositiveI64,
314
315    /// Стоимость большой крутки в gacha-билетах (она же — очки прогресса).
316    #[schemars(title = "Стоимость большой крутки")]
317    pub big_roll_cost: PositiveI64,
318
319    /// Бонусные дропы в большой крутке сверх стоимости. Итого дропов = cost + bonus.
320    #[schemars(title = "Бонусные дропы в большой крутке")]
321    pub big_roll_bonus_drops: u8,
322
323    /// Максимальное число слотов вишлиста.
324    #[schemars(title = "Количество слотов вишлиста")]
325    pub wishlist_slots: u8,
326
327    /// Множитель веса для способностей из вишлиста при розыгрыше.
328    /// Значение `2.0` означает, что вишлист-абилка выпадает вдвое чаще обычной.
329    #[schemars(title = "Множитель веса вишлиста")]
330    pub wishlist_weight_multiplier: WeightMultiplier,
331
332    /// Валюта для прокачки уровней слотов способностей (Stardust).
333    #[schemars(
334        title = "Валюта прокачки слотов способностей",
335        schema_with = "currency_link_id_schema"
336    )]
337    pub slot_upgrade_currency_id: CurrencyId,
338
339    /// Максимальный уровень слота способности.
340    #[schemars(title = "Максимальный уровень слота способности")]
341    pub slot_max_level: PositiveI64,
342
343    /// Стоимость повышения слота до каждого уровня.
344    /// Индекс `0` соответствует прокачке на уровень `1`.
345    #[schemars(title = "Стоимости уровней слотов способностей")]
346    pub slot_level_costs: Vec<PositiveI64>,
347
348    /// Бонус к `AbilityLevel` по уровню слота.
349    /// Индекс = уровень слота, значение = дополнительный уровень способности.
350    #[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    /// Стоимость маленькой крутки в gacha-билетах.
366    /// Она же — количество дропов и очки прогресса (тикеты = поинты = дропы).
367    #[schemars(title = "Стоимость маленькой крутки")]
368    pub small_roll_cost: PositiveI64,
369
370    /// Стоимость большой крутки в gacha-билетах (она же — очки прогресса).
371    #[schemars(title = "Стоимость большой крутки")]
372    pub big_roll_cost: PositiveI64,
373
374    /// Бонусные дропы в большой крутке сверх стоимости. Итого дропов = cost + bonus.
375    #[schemars(title = "Бонусные дропы в большой крутке")]
376    pub big_roll_bonus_drops: u8,
377
378    /// Максимальное число слотов вишлиста.
379    #[schemars(title = "Количество слотов вишлиста")]
380    pub wishlist_slots: u8,
381
382    /// Множитель веса для петов из вишлиста при розыгрыше.
383    /// Значение `2.0` означает, что вишлист-пет выпадает вдвое чаще обычного.
384    #[schemars(title = "Множитель веса вишлиста")]
385    pub wishlist_weight_multiplier: WeightMultiplier,
386
387    /// BAL-032: after this many consecutive rolls that produced no NEW pet, the
388    /// next roll is forced onto a template the player does not own yet.
389    ///
390    /// A pure safety net on top of the ordinary weights — the wishlist `×2`
391    /// multiplier is unchanged, and the counter resets on any first copy however
392    /// it was obtained. `0` disables it.
393    #[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}