configs/
ability_stones.rs1use essences::flip::WorldSide;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use tsify_next::Tsify;
5
6#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
11pub struct AbilityStoneSocketSettings {
12 #[schemars(title = "Сторона сокета (Real / Fantasy)")]
13 pub side: WorldSide,
14
15 #[schemars(
16 title = "Уровень способности, открывающий сокет",
17 description = "Сокет недоступен, пока способность не достигла этого уровня."
18 )]
19 pub unlock_ability_level: i64,
20}
21
22#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, Tsify)]
25pub struct AbilityStoneSettings {
26 #[schemars(
27 title = "Глава разблокировки камней способностей",
28 description = "До этой главы камни нельзя вставлять, снимать и улучшать, а выдача за главы не работает."
29 )]
30 pub unlock_chapter: i64,
31
32 #[schemars(title = "Сокеты обычной способности")]
35 pub sockets: Vec<AbilityStoneSocketSettings>,
36
37 #[schemars(
40 title = "Сокеты классовой способности",
41 description = "Отдельная лестница сокетов для классовых способностей: их уровень ограничен ниже обычных."
42 )]
43 pub class_sockets: Vec<AbilityStoneSocketSettings>,
44
45 #[schemars(
49 title = "Стоимость уровней камня в копиях",
50 description = "Элемент i — сколько сырых копий нужно для перехода на уровень i + 2. Длина задаёт максимальный уровень."
51 )]
52 pub upgrade_copies: Vec<i64>,
53
54 #[schemars(
61 title = "Период выдачи камней за главы (заглушка)",
62 description = "Каждые N пройденных уровней глав игрок получает одну копию камня способности. 0 отключает выдачу."
63 )]
64 pub chapter_reward_period: i64,
65}
66
67impl AbilityStoneSettings {
68 pub const fn is_unlocked(&self, chapter_level: i64) -> bool {
69 chapter_level >= self.unlock_chapter
70 }
71
72 pub fn max_level(&self) -> i64 {
74 self.upgrade_copies.len() as i64 + 1
75 }
76
77 pub fn copies_for_next_level(&self, level: i64) -> Option<i64> {
80 if level < 1 {
81 return None;
82 }
83 self.upgrade_copies.get((level - 1) as usize).copied()
84 }
85
86 pub fn sockets_for(&self, is_class_ability: bool) -> &[AbilityStoneSocketSettings] {
88 if is_class_ability {
89 &self.class_sockets
90 } else {
91 &self.sockets
92 }
93 }
94
95 pub fn socket(
96 &self,
97 is_class_ability: bool,
98 socket_index: i64,
99 ) -> Option<&AbilityStoneSocketSettings> {
100 usize::try_from(socket_index)
101 .ok()
102 .and_then(|index| self.sockets_for(is_class_ability).get(index))
103 }
104
105 pub fn is_socket_unlocked(
107 &self,
108 is_class_ability: bool,
109 socket_index: i64,
110 ability_level: i64,
111 ) -> bool {
112 self.socket(is_class_ability, socket_index)
113 .is_some_and(|socket| ability_level >= socket.unlock_ability_level)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn ladder(levels: &[i64]) -> Vec<AbilityStoneSocketSettings> {
122 levels
123 .iter()
124 .enumerate()
125 .map(|(index, level)| AbilityStoneSocketSettings {
126 side: if index % 2 == 0 {
127 WorldSide::Real
128 } else {
129 WorldSide::Fantasy
130 },
131 unlock_ability_level: *level,
132 })
133 .collect()
134 }
135
136 fn settings() -> AbilityStoneSettings {
137 AbilityStoneSettings {
138 unlock_chapter: 45,
139 sockets: ladder(&[2, 4, 8, 16]),
140 class_sockets: ladder(&[2, 4, 6, 7]),
141 upgrade_copies: vec![2, 3, 5],
142 chapter_reward_period: 5,
143 }
144 }
145
146 #[test]
147 fn is_unlocked_gates_by_chapter() {
148 let settings = settings();
149 assert!(!settings.is_unlocked(44));
150 assert!(settings.is_unlocked(45));
151 }
152
153 #[test]
154 fn class_ladder_is_fully_reachable_at_the_class_cap() {
155 let settings = settings();
156 for index in 0..4 {
158 assert!(settings.is_socket_unlocked(true, index, 7));
159 }
160 assert!(settings.is_socket_unlocked(false, 1, 7));
162 assert!(!settings.is_socket_unlocked(false, 2, 7));
163 assert!(!settings.is_socket_unlocked(false, 3, 7));
164 }
165
166 #[test]
167 fn each_kind_reads_its_own_ladder() {
168 let settings = settings();
169 assert_eq!(settings.socket(false, 2).unwrap().unlock_ability_level, 8);
170 assert_eq!(settings.socket(true, 2).unwrap().unlock_ability_level, 6);
171 assert!(settings.socket(false, 4).is_none());
172 }
173}