essences/
gatings.rs

1use crate::prelude::*;
2
3// NOTE: when adding a new gated feature here (or to the nested
4// `NavBarNavigation` / `SideBarNavigation` / `AutoChestGatings` /
5// `CastleGating` structs below), decide whether it belongs in the
6// `feature_unlock_snapshot` analytics event and update
7// `Gatings::feature_unlock_thresholds` accordingly. Unlike the old
8// open-coded array, that method now destructures every field without a
9// `..` rest pattern, so adding a field here is a compile error there
10// until you handle it — the build-time signal that used to be missing.
11#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
12pub struct Gatings {
13    #[schemars(title = "Гейтинги навбара")]
14    pub navbar_navigation: NavBarNavigation,
15    #[schemars(title = "Гейтинги сайдбара")]
16    pub sidebar_navigation: SideBarNavigation,
17    #[schemars(title = "Гейтинги авточеста")]
18    pub autochest: AutoChestGatings,
19    #[schemars(title = "Чаптер, на котором открывается кнопка афк наград")]
20    pub afk_rewards_button_unlock_chapter: i64,
21    #[schemars(title = "Чаптер, на котором открывается пати")]
22    pub party_unlock_chapter: i64,
23    #[schemars(title = "Чаптер, на котором открывается кнопка магазина")]
24    pub shop_button_unlock_chapter: i64,
25    #[schemars(title = "Чаптер, на котором открывается кнопка дневного буста")]
26    pub daily_boost_button_unlock_chapter: i64,
27
28    #[schemars(title = "Чаптер, на котором открывается окно кастомизации")]
29    pub customization_screen_unlock_chapter: i64,
30}
31
32#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
33pub struct NavBarNavigation {
34    #[schemars(title = "Чаптер, на котором открывается кнопка профиля")]
35    pub hero_button_unlock_chapter: i64,
36
37    #[schemars(title = "Чаптер, на котором открывается кнопка скилов")]
38    pub skills_button_unlock_chapter: i64,
39
40    #[schemars(title = "Чаптер, на котором открывается кнопка данжа")]
41    pub dungeon_button_unlock_chapter: i64,
42
43    #[schemars(title = "Чаптер, на котором открывается кнопка ведьмы")]
44    pub summon_button_unlock_chapter: i64,
45
46    #[schemars(title = "Чаптер, на котором открывается кнопка петов")]
47    pub pets_button_unlock_chapter: i64,
48
49    #[schemars(title = "Гейтинги замка")]
50    pub castle: CastleGating,
51}
52
53#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
54pub struct SideBarNavigation {
55    #[schemars(title = "Чаптер, на котором открывается кнопка квестов")]
56    pub quests_button_unlock_chapter: i64,
57
58    #[schemars(title = "Чаптер, на котором открывается кнопка прогресс пасса")]
59    pub progress_pass_button_unlock_chapter: i64,
60
61    #[schemars(title = "Чаптер, на котором открывается кнопка арены")]
62    pub arena_button_unlock_chapter: i64,
63
64    #[schemars(title = "Чаптер, на котором открывается кнопка рейтингов")]
65    pub ratings_button_unlock_chapter: i64,
66
67    #[schemars(title = "Чаптер, на котором открывается кнопка почты")]
68    pub mail_button_unlock_chapter: i64,
69}
70
71#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
72pub struct AutoChestGatings {
73    #[schemars(title = "Уровень персонажа, при котором открывается авточест")]
74    pub autochest_button_unlock_character_level: i64,
75
76    #[schemars(title = "Чаптер, на котором открывается кнопка апгрейда сундука")]
77    pub chest_upgrade_button_unlock_chapter: i64,
78
79    #[schemars(title = "Показывать подсказку клика по сундуку до stage (не включительно)")]
80    pub show_chest_click_tip_until_stage: i64,
81
82    #[schemars(title = "Чаптер, на котором открывается фильтр по редкости")]
83    pub rarity_filter_unlock: i64,
84
85    #[schemars(title = "Чаптер, на котором открывается гарантированный стат")]
86    pub guaranteed_stat_unlock: i64,
87
88    #[schemars(title = "Чаптер, на котором открывается первый дополнительный стат")]
89    pub first_additional_stat_unlock: i64,
90
91    #[schemars(title = "Чаптер, на котором открывается второй дополнительный стат")]
92    pub second_additional_stat_unlock: i64,
93
94    #[schemars(title = "Чаптер, на котором открывается третий дополнительный стат")]
95    pub third_additional_stat_unlock: i64,
96}
97
98#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
99pub struct CastleGating {
100    #[schemars(title = "Чаптер, на котором открывается кнопка замка")]
101    pub castle_button_unlock_chapter: i64,
102
103    #[schemars(title = "Чаптер, на котором открывается дерево талантов")]
104    pub talent_tree_unlock_chapter: i64,
105
106    #[schemars(title = "Чаптер, на котором открывается статуя")]
107    pub statue_unlock_chapter: i64,
108}
109
110impl Gatings {
111    /// Flat `(analytics_name, unlock_chapter)` list backing the
112    /// `feature_unlock_snapshot` analytics event emitted in
113    /// `monolith::ws::connection::complete_auth`.
114    ///
115    /// The names are a curated, stable analytics contract (downstream
116    /// queries `MIN(timestamp) PER (character_id, feature)` on them), so
117    /// they intentionally differ from the raw field names.
118    ///
119    /// This destructures every field of `Gatings` and its nested structs
120    /// **without** a `..` rest pattern on purpose: adding a new gated
121    /// field anywhere is then a compile error here until the author
122    /// decides whether it belongs in the snapshot. That is the
123    /// build-time signal the previous open-coded array lacked. Fields
124    /// that are deliberately not emitted are bound to `_` with a reason.
125    pub fn feature_unlock_thresholds(&self) -> Vec<(&'static str, i64)> {
126        let Gatings {
127            navbar_navigation,
128            sidebar_navigation,
129            autochest,
130            afk_rewards_button_unlock_chapter,
131            party_unlock_chapter,
132            shop_button_unlock_chapter,
133            daily_boost_button_unlock_chapter,
134            customization_screen_unlock_chapter,
135        } = self;
136
137        let NavBarNavigation {
138            hero_button_unlock_chapter,
139            skills_button_unlock_chapter,
140            dungeon_button_unlock_chapter,
141            summon_button_unlock_chapter,
142            pets_button_unlock_chapter,
143            castle,
144        } = navbar_navigation;
145
146        let CastleGating {
147            castle_button_unlock_chapter,
148            talent_tree_unlock_chapter,
149            statue_unlock_chapter,
150        } = castle;
151
152        let SideBarNavigation {
153            quests_button_unlock_chapter,
154            // Not currently emitted to feature_unlock_snapshot. If this
155            // should be tracked, add it to the returned list below.
156            progress_pass_button_unlock_chapter: _,
157            arena_button_unlock_chapter,
158            ratings_button_unlock_chapter,
159            mail_button_unlock_chapter,
160        } = sidebar_navigation;
161
162        let AutoChestGatings {
163            // Auto-chest now unlocks by character LEVEL (enforced server-side in
164            // handle_enable_auto_chest + AutoChestFilter::validate), so it is not
165            // a chapter threshold and stays out of this chapter-keyed list.
166            autochest_button_unlock_character_level: _,
167            chest_upgrade_button_unlock_chapter,
168            // A UI hint window, not a feature unlock.
169            show_chest_click_tip_until_stage: _,
170            rarity_filter_unlock,
171            guaranteed_stat_unlock,
172            // Additional-stat unlocks are intentionally outside the snapshot.
173            first_additional_stat_unlock: _,
174            second_additional_stat_unlock: _,
175            third_additional_stat_unlock: _,
176        } = autochest;
177
178        vec![
179            ("afk_rewards", *afk_rewards_button_unlock_chapter),
180            ("party", *party_unlock_chapter),
181            ("shop", *shop_button_unlock_chapter),
182            ("daily_boost", *daily_boost_button_unlock_chapter),
183            ("hero_button", *hero_button_unlock_chapter),
184            ("skills_button", *skills_button_unlock_chapter),
185            ("dungeon_button", *dungeon_button_unlock_chapter),
186            ("summon_button", *summon_button_unlock_chapter),
187            ("pets_button", *pets_button_unlock_chapter),
188            ("castle_button", *castle_button_unlock_chapter),
189            ("talent_tree", *talent_tree_unlock_chapter),
190            ("statue", *statue_unlock_chapter),
191            ("quests_button", *quests_button_unlock_chapter),
192            ("arena_button", *arena_button_unlock_chapter),
193            ("ratings_button", *ratings_button_unlock_chapter),
194            ("mail_button", *mail_button_unlock_chapter),
195            ("chest_upgrade_button", *chest_upgrade_button_unlock_chapter),
196            ("autochest_rarity_filter", *rarity_filter_unlock),
197            ("autochest_guaranteed_stat", *guaranteed_stat_unlock),
198            ("customization_screen", *customization_screen_unlock_chapter),
199        ]
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn feature_unlock_thresholds_are_stable_and_unique() {
209        // Locks the analytics contract for `feature_unlock_snapshot`.
210        // If you intentionally add/remove a tracked feature, update this
211        // count to match.
212        let list = Gatings::default().feature_unlock_thresholds();
213        assert_eq!(list.len(), 20, "tracked feature count changed");
214
215        let mut names: Vec<&str> = list.iter().map(|(name, _)| *name).collect();
216        names.sort_unstable();
217        let mut unique = names.clone();
218        unique.dedup();
219        assert_eq!(names, unique, "feature analytics names must be unique");
220    }
221}