essences/
abilities.rs

1use crate::prelude::*;
2
3use enum_iterator::Sequence;
4use strum_macros::{Display, EnumIter, EnumString};
5
6use std::collections::{BTreeMap, HashMap};
7
8#[declare]
9pub type AbilityId = Uuid;
10
11#[declare]
12pub type AbilitySlotId = usize;
13
14#[declare]
15pub type AbilityRarityId = Uuid;
16
17#[derive(
18    Debug,
19    Clone,
20    Copy,
21    EnumString,
22    Sequence,
23    Display,
24    Deserialize,
25    Serialize,
26    Hash,
27    Eq,
28    PartialEq,
29    EnumIter,
30    Default,
31    JsonSchema,
32    Tsify,
33)]
34#[tsify(namespace)]
35pub enum AbilityFightUiVisibility {
36    #[default]
37    Slotted,
38    Class,
39    Hidden,
40}
41
42impl AbilityFightUiVisibility {
43    pub fn is_player_equippable(&self) -> bool {
44        match self {
45            AbilityFightUiVisibility::Slotted => true,
46            AbilityFightUiVisibility::Class => false,
47            AbilityFightUiVisibility::Hidden => false,
48        }
49    }
50
51    pub fn is_slotted(&self) -> bool {
52        match self {
53            AbilityFightUiVisibility::Slotted => true,
54            AbilityFightUiVisibility::Class => false,
55            AbilityFightUiVisibility::Hidden => false,
56        }
57    }
58}
59
60#[derive(
61    Debug,
62    Clone,
63    Copy,
64    EnumString,
65    Sequence,
66    Display,
67    Deserialize,
68    Serialize,
69    Hash,
70    Eq,
71    PartialEq,
72    EnumIter,
73    Default,
74    JsonSchema,
75    Tsify,
76)]
77#[tsify(namespace)]
78pub enum AbilityCastType {
79    #[default]
80    NoAnimation,
81    Basic,
82    Spell,
83}
84
85/// Whom an ability can target. Sourced from the ability content `target_type`
86/// and consumed by the fight target-selection (`is_valid_target` / `try_cast`).
87#[derive(
88    Debug,
89    Clone,
90    Copy,
91    EnumString,
92    Sequence,
93    Display,
94    Deserialize,
95    Serialize,
96    Hash,
97    Eq,
98    PartialEq,
99    EnumIter,
100    Default,
101    JsonSchema,
102    Tsify,
103)]
104#[tsify(namespace)]
105pub enum AbilityTargetType {
106    #[default]
107    Enemy,
108    Ally,
109    // `Self` is a reserved word in Rust; keep the wire/content value `Self`.
110    #[serde(rename = "Self")]
111    #[strum(serialize = "Self")]
112    Zelf,
113}
114
115impl AbilityTargetType {
116    /// The content/wire string ("Enemy" / "Ally" / "Self") used by the fight
117    /// targeting code.
118    pub fn as_str(&self) -> &'static str {
119        match self {
120            AbilityTargetType::Enemy => "Enemy",
121            AbilityTargetType::Ally => "Ally",
122            AbilityTargetType::Zelf => "Self",
123        }
124    }
125}
126
127/// Тип крутки гачи способностей.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Tsify)]
129#[tsify(namespace)]
130pub enum AbilityCaseRollType {
131    Small,
132    Big,
133}
134
135// NOTE: no `Eq`/`Hash` — `eff` is an `f64`. `AbilityRarity` is only ever held in
136// `Vec<AbilityRarity>`, never hashed or used as a map key, so this is safe.
137#[derive(Default, PartialEq, Debug, Clone, Serialize, Deserialize, Tsify, JsonSchema)]
138pub struct AbilityRarity {
139    #[schemars(schema_with = "id_schema")]
140    pub id: AbilityRarityId,
141
142    #[schemars(title = "Название редкости")]
143    pub name: i18n::I18nString,
144
145    #[schemars(title = "Сортировка")]
146    pub order: u64,
147
148    #[schemars(title = "Цвет редкости", schema_with = "color_schema")]
149    pub color: String,
150
151    #[schemars(title = "Цвет заднего фона редкости", schema_with = "color_schema")]
152    pub bg_color: String,
153
154    #[schemars(title = "Иконка рамки", schema_with = "webp_url_schema")]
155    pub icon_url: String,
156
157    #[schemars(title = "Рамка", schema_with = "asset_ability_rarity_icon_schema")]
158    pub icon_path: String,
159
160    #[schemars(
161        title = "Квадратная рамка",
162        schema_with = "asset_ability_rarity_square_icon_schema"
163    )]
164    pub square_icon_path: String,
165
166    #[schemars(
167        title = "Эффективность редкости (eff)",
168        description = "Множитель урона способностей этой редкости (ability_rarity_eff). Используется в balance::ability_eff."
169    )]
170    #[serde(default)]
171    pub eff: f64,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema, Default)]
175#[tsify(from_wasm_abi)]
176pub struct AbilityTemplate {
177    #[schemars(schema_with = "id_schema")]
178    pub id: AbilityId,
179
180    #[schemars(title = "Видимость в UI боя")]
181    pub is_fight_ui_visible: bool,
182
183    #[schemars(title = "Тип видимости в UI боя")]
184    pub fight_ui_visibility: AbilityFightUiVisibility,
185
186    #[schemars(title = "Имя способности")]
187    pub name: i18n::I18nString,
188
189    #[schemars(
190        title = "Нативная функция старта каста",
191        description = "Имя нативной функции категории start_cast_ability, выполняющей старт каста способности.",
192        schema_with = "start_cast_ability_ref_schema"
193    )]
194    #[serde(default)]
195    pub start_behavior: Option<String>,
196
197    #[schemars(
198        title = "Нативная функция каста способности",
199        description = "Имя нативной функции категории cast_ability, выполняющей каст способности.",
200        schema_with = "cast_ability_ref_schema"
201    )]
202    #[serde(default)]
203    pub behavior: Option<String>,
204
205    #[schemars(
206        title = "Ссылка на иконку способности",
207        schema_with = "webp_url_schema"
208    )]
209    pub icon_url: String,
210
211    #[schemars(title = "Иконка", schema_with = "asset_ability_icon_schema")]
212    pub icon_path: String,
213
214    #[schemars(title = "Перезарядка способности")]
215    pub cooldown: u64,
216
217    #[schemars(title = "Показывается в окне гачи, умеет выпадать из гачи")]
218    pub is_gacha_ability: bool,
219
220    #[schemars(title = "Доступна ботам (для генерации оппонентов)")]
221    #[serde(default)]
222    pub available_to_bots: bool,
223
224    #[schemars(title = "Id редкости", schema_with = "ability_rarity_link_id_schema")]
225    pub rarity_id: AbilityRarityId,
226
227    #[schemars(title = "Описание способности")]
228    pub description: i18n::I18nString,
229
230    #[schemars(
231        title = "Скрипт, возвращающий значения для подстановки в описание",
232        schema_with = "option_script_schema"
233    )]
234    pub description_values_script: Option<String>,
235
236    #[schemars(title = "VFX", schema_with = "asset_vfx_object_schema")]
237    pub vfx_object_path: String,
238
239    #[schemars(title = "Тип каста")]
240    pub cast_type: AbilityCastType,
241
242    #[schemars(title = "Тип цели (Enemy / Ally / Self)")]
243    #[serde(default)]
244    pub target_type: AbilityTargetType,
245
246    #[schemars(title = "Дальность каста (в клетках)")]
247    #[serde(default)]
248    pub range: i64,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
252#[tsify(from_wasm_abi)]
253pub struct Ability {
254    pub template_id: AbilityId,
255    pub level: i64,
256    pub shards_amount: i64,
257}
258
259impl Ability {
260    pub fn from_template(
261        ability_template: &AbilityTemplate,
262        level: Option<i64>,
263        shards_amount: Option<i64>,
264    ) -> Self {
265        Ability {
266            template_id: ability_template.id,
267            level: level.unwrap_or(1),
268            shards_amount: shards_amount.unwrap_or(0),
269        }
270    }
271}
272
273#[derive(Clone, Debug, Serialize, Deserialize, Eq, JsonSchema, Tsify)]
274pub struct ActiveAbility {
275    pub ability: Ability,
276    #[cfg_attr(target_arch = "wasm32", schemars(skip))]
277    pub deadline: Option<chrono::DateTime<chrono::Utc>>,
278    pub slot_id: Option<AbilitySlotId>,
279}
280
281impl PartialEq for ActiveAbility {
282    fn eq(&self, other: &Self) -> bool {
283        self.ability == other.ability
284            && self.slot_id == other.slot_id
285            && self.deadline.zip(other.deadline).map_or(
286                self.deadline.is_none() && other.deadline.is_none(),
287                |(a, b)| a.signed_duration_since(b).abs() <= chrono::TimeDelta::milliseconds(250),
288            )
289    }
290}
291
292#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
293pub struct UpgradedAbilitiesMap(pub HashMap<AbilityId, (i64, i64)>);
294
295impl UpgradedAbilitiesMap {
296    pub fn upgrade(&mut self, id: AbilityId, level: i64) {
297        self.0
298            .entry(id)
299            .and_modify(|(_, max)| {
300                *max += 1;
301            })
302            .or_insert((level, level + 1));
303    }
304
305    pub fn insert(&mut self, id: AbilityId, levels: (i64, i64)) {
306        self.0.insert(id, levels);
307    }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
311pub struct AbilityShard {
312    pub ability_id: AbilityId,
313    pub shards_amount: i64,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
317pub struct AbilityDrop {
318    pub template: AbilityTemplate,
319    pub is_new: bool,
320    pub evolved_from: Option<AbilityTemplate>,
321    pub is_checkpoint: bool,
322}
323
324#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, Default)]
325#[tsify(from_wasm_abi)]
326pub struct EquippedAbilities {
327    pub slotted: BTreeMap<AbilitySlotId, Ability>,
328    pub unslotted: Vec<Ability>,
329}
330
331impl EquippedAbilities {
332    pub fn new() -> Self {
333        Self {
334            slotted: BTreeMap::new(),
335            unslotted: Vec::new(),
336        }
337    }
338
339    pub fn add(&mut self, ability: Ability, slot_id: Option<AbilitySlotId>) {
340        if let Some(slot_id) = slot_id {
341            self.slotted.insert(slot_id, ability);
342        } else {
343            self.unslotted.push(ability);
344        }
345    }
346
347    pub fn to_vec(&self) -> Vec<&Ability> {
348        self.unslotted.iter().chain(self.slotted.values()).collect()
349    }
350
351    pub fn to_vec_mut(&mut self) -> Vec<&mut Ability> {
352        self.unslotted
353            .iter_mut()
354            .chain(self.slotted.values_mut())
355            .collect()
356    }
357
358    pub fn get_by_slot_id(&self, slot_id: AbilitySlotId) -> Option<&Ability> {
359        self.slotted.get(&slot_id)
360    }
361
362    pub fn has_ability(&self, ability_id: AbilityId) -> bool {
363        self.slotted.values().any(|a| a.template_id == ability_id)
364            || self.unslotted.iter().any(|a| a.template_id == ability_id)
365    }
366
367    pub fn get_mut_by_id(&mut self, ability_id: AbilityId) -> Option<&mut Ability> {
368        self.slotted
369            .values_mut()
370            .find(|a| a.template_id == ability_id)
371            .or_else(|| {
372                self.unslotted
373                    .iter_mut()
374                    .find(|a| a.template_id == ability_id)
375            })
376    }
377
378    pub fn remove_by_slot_id(&mut self, slot_id: AbilitySlotId) -> Option<Ability> {
379        self.slotted.remove(&slot_id)
380    }
381
382    pub fn len(&self) -> usize {
383        self.slotted.len() + self.unslotted.len()
384    }
385
386    pub fn is_empty(&self) -> bool {
387        self.slotted.is_empty() && self.unslotted.is_empty()
388    }
389
390    pub fn clear(&mut self) {
391        self.slotted.clear();
392        self.unslotted.clear();
393    }
394}