configs/
collection_power.rs

1use essences::{items::AttributeId, pets::PetRarityId, stones::StoneTier};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use tsify_next::Tsify;
5
6/// The rarity weight of one equipment-stone tier.
7///
8/// Weight prices only the PASSIVE collection value of a template. It changes
9/// nothing about the stone's effect, its drop chance or its upgrade cost.
10#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
11pub struct StoneTierCollectionWeight {
12    #[schemars(title = "Тир камня")]
13    pub tier: StoneTier,
14
15    #[schemars(
16        title = "Вес тира",
17        description = "Во сколько раз владение камнем этого тира ценнее обычного. Только для коллекционной силы."
18    )]
19    pub weight: f64,
20}
21
22/// The rarity weight of one pet rarity. Same rule as
23/// [`StoneTierCollectionWeight`]: passive collection value only.
24#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
25pub struct PetRarityCollectionWeight {
26    #[schemars(
27        title = "Id редкости пета",
28        schema_with = "schema_loader::pet_rarity_link_id_schema"
29    )]
30    pub rarity_id: PetRarityId,
31
32    #[schemars(
33        title = "Вес редкости",
34        description = "Во сколько раз владение петом этой редкости ценнее обычного. Только для коллекционной силы."
35    )]
36    pub weight: f64,
37}
38
39/// One catalog's share of the Collection Power budget.
40///
41/// `k` is the bonus paid per template per unit of weight at FULL progress, so a
42/// family's full-catalog bonus is `k × Σ weight`. It is an authored constant,
43/// deliberately not a budget divided by the live catalog size: new content must
44/// have its own budget signed off rather than silently diluting — or silently
45/// inflating — what the current catalog already pays. A template added without
46/// a re-authored `k` simply extends the family past its signed budget, which is
47/// the visible failure the `collection_power` tests catch.
48#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
49pub struct CollectionPowerFamily {
50    #[schemars(
51        title = "Коэффициент семейства (k_family)",
52        description = "Бонус за один шаблон на единицу веса при полном ранге. Полный бонус семейства = k × сумма весов каталога."
53    )]
54    pub k: f64,
55}
56
57/// Collection Power: a small permanent stat bonus for OWNING catalog content,
58/// paid whether or not the template is equipped.
59///
60/// Loadout keeps its job — active effects, synergies and the difference between
61/// builds. This pays only for breadth and rank, in three flat percentages on
62/// Attack / HP / Armor, so a stone or law that no longer fits the build is
63/// still worth having.
64///
65/// Deliberately NOT here: artifacts, which already pay an ownership bonus of
66/// their own (`ArtifactTemplate::ownership_bonuses`) and would be counted twice;
67/// pet facets, whose value arrives through the pet that carries them; cores,
68/// bridges and sockets, which are progression rather than collectible
69/// templates; and equipment items, which are constantly replaced and sold.
70#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
71pub struct CollectionPowerSettings {
72    /// The share of a template's contribution that the FIRST copy pays, with
73    /// the rest spread linearly across its rank ladder. Breadth is worth
74    /// something immediately; depth is worth most of it.
75    #[schemars(
76        title = "Доля за первое владение",
77        description = "Какую часть вклада шаблона даёт первая копия. Остаток распределён линейно по его рангам."
78    )]
79    pub first_ownership_share: f64,
80
81    /// The attributes `M_collection` multiplies. Read by CODE through
82    /// `<code>.mod`, the same channel artifact ownership uses, so the bonus
83    /// reaches combat, the displayed power scalar and PvP matchmaking at once.
84    #[schemars(
85        title = "Атрибуты, умножаемые коллекционной силой",
86        description = "Обычно Attack / HP / Armor. Бонус применяется как общий множитель к каждому из них.",
87        schema_with = "schema_loader::attribute_link_id_array_schema"
88    )]
89    pub multiplied_attribute_ids: Vec<AttributeId>,
90
91    #[schemars(title = "Камни экипировки (триггеры и эффекты)")]
92    pub equipment_stones: CollectionPowerFamily,
93
94    #[schemars(title = "Законы")]
95    pub laws: CollectionPowerFamily,
96
97    #[schemars(title = "Камни артефактов")]
98    pub artifact_stones: CollectionPowerFamily,
99
100    #[schemars(title = "Камни способностей")]
101    pub ability_stones: CollectionPowerFamily,
102
103    #[schemars(title = "Гача-способности")]
104    pub gacha_abilities: CollectionPowerFamily,
105
106    #[schemars(title = "Петы")]
107    pub pets: CollectionPowerFamily,
108
109    #[schemars(title = "Веса тиров камней экипировки")]
110    pub stone_tier_weights: Vec<StoneTierCollectionWeight>,
111
112    #[schemars(title = "Веса редкостей петов")]
113    pub pet_rarity_weights: Vec<PetRarityCollectionWeight>,
114}
115
116impl CollectionPowerSettings {
117    /// Collection weight of an equipment-stone tier. An unweighted tier pays
118    /// nothing — `GameConfig::validate_collection_power` requires a row per
119    /// tier, so this is a safety net rather than a reachable state.
120    pub fn stone_tier_weight(&self, tier: StoneTier) -> f64 {
121        self.stone_tier_weights
122            .iter()
123            .find(|row| row.tier == tier)
124            .map_or(0.0, |row| row.weight)
125    }
126
127    /// Collection weight of a pet rarity. Same rule as
128    /// [`Self::stone_tier_weight`]: an unweighted rarity pays nothing.
129    pub fn pet_rarity_weight(&self, rarity_id: PetRarityId) -> f64 {
130        self.pet_rarity_weights
131            .iter()
132            .find(|row| row.rarity_id == rarity_id)
133            .map_or(0.0, |row| row.weight)
134    }
135
136    /// Progress of one owned template, in `0..=1`.
137    ///
138    /// `level` is the template's current rank and `max_level` the top of its
139    /// family's ladder. A one-rung ladder pays the full contribution on the
140    /// first copy — there is no rank left to earn the rest.
141    pub fn progress(&self, level: i64, max_level: i64) -> f64 {
142        let first = self.first_ownership_share;
143        if max_level <= 1 {
144            return 1.0;
145        }
146        let level = level.clamp(1, max_level);
147        first + (1.0 - first) * ((level - 1) as f64 / (max_level - 1) as f64)
148    }
149}