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(
74 Debug,
75 Clone,
76 Copy,
77 EnumString,
78 Sequence,
79 Display,
80 Deserialize,
81 Serialize,
82 Hash,
83 Eq,
84 PartialEq,
85 Ord,
86 PartialOrd,
87 EnumIter,
88 Default,
89 JsonSchema,
90 Tsify,
91)]
92#[tsify(namespace)]
93pub enum AbilityTag {
94 #[default]
96 Heal,
97 Damage,
99 Aoe,
101 MultiTarget,
106 Buff,
108 Debuff,
110 Control,
112 Pet,
114 Projectile,
116 Targeted,
118 Duration,
120 Summon,
122 SelfCast,
125 Protection,
127 Channelled,
131}
132
133#[derive(
134 Debug,
135 Clone,
136 Copy,
137 EnumString,
138 Sequence,
139 Display,
140 Deserialize,
141 Serialize,
142 Hash,
143 Eq,
144 PartialEq,
145 EnumIter,
146 Default,
147 JsonSchema,
148 Tsify,
149)]
150#[tsify(namespace)]
151pub enum AbilityCastType {
152 #[default]
153 NoAnimation,
154 Basic,
155 Spell,
156}
157
158#[derive(
161 Debug,
162 Clone,
163 Copy,
164 EnumString,
165 Sequence,
166 Display,
167 Deserialize,
168 Serialize,
169 Hash,
170 Eq,
171 PartialEq,
172 EnumIter,
173 Default,
174 JsonSchema,
175 Tsify,
176)]
177#[tsify(namespace)]
178pub enum AbilityTargetType {
179 #[default]
180 Enemy,
181 Ally,
182 #[serde(rename = "Self")]
184 #[strum(serialize = "Self")]
185 Zelf,
186}
187
188impl AbilityTargetType {
189 pub fn as_str(&self) -> &'static str {
192 match self {
193 AbilityTargetType::Enemy => "Enemy",
194 AbilityTargetType::Ally => "Ally",
195 AbilityTargetType::Zelf => "Self",
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Tsify)]
202#[tsify(namespace)]
203pub enum AbilityCaseRollType {
204 Small,
205 Big,
206}
207
208#[derive(Default, PartialEq, Debug, Clone, Serialize, Deserialize, Tsify, JsonSchema)]
211pub struct AbilityRarity {
212 #[schemars(schema_with = "id_schema")]
213 pub id: AbilityRarityId,
214
215 #[schemars(title = "Название редкости")]
216 pub name: i18n::I18nString,
217
218 #[schemars(title = "Сортировка")]
219 pub order: u64,
220
221 #[schemars(title = "Цвет редкости", schema_with = "color_schema")]
222 pub color: String,
223
224 #[schemars(title = "Цвет заднего фона редкости", schema_with = "color_schema")]
225 pub bg_color: String,
226
227 #[schemars(title = "Иконка рамки", schema_with = "webp_url_schema")]
228 pub icon_url: String,
229
230 #[schemars(title = "Рамка", schema_with = "asset_ability_rarity_icon_schema")]
231 pub icon_path: String,
232
233 #[schemars(
234 title = "Квадратная рамка",
235 schema_with = "asset_ability_rarity_square_icon_schema"
236 )]
237 pub square_icon_path: String,
238
239 #[schemars(
240 title = "Эффективность редкости (eff)",
241 description = "Множитель урона способностей этой редкости (ability_rarity_eff). Используется в balance::ability_eff."
242 )]
243 #[serde(default)]
244 pub eff: f64,
245}
246
247#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema, Default)]
248#[tsify(from_wasm_abi)]
249pub struct AbilityTemplate {
250 #[schemars(schema_with = "id_schema")]
251 pub id: AbilityId,
252
253 #[schemars(title = "Видимость в UI боя")]
254 pub is_fight_ui_visible: bool,
255
256 #[schemars(title = "Тип видимости в UI боя")]
257 pub fight_ui_visibility: AbilityFightUiVisibility,
258
259 #[schemars(title = "Имя способности")]
260 pub name: i18n::I18nString,
261
262 #[schemars(
263 title = "Нативная функция старта каста",
264 description = "Имя нативной функции категории start_cast_ability, выполняющей старт каста способности.",
265 schema_with = "start_cast_ability_ref_schema"
266 )]
267 #[serde(default)]
268 pub start_behavior: Option<String>,
269
270 #[schemars(
271 title = "Нативная функция каста способности",
272 description = "Имя нативной функции категории cast_ability, выполняющей каст способности.",
273 schema_with = "cast_ability_ref_schema"
274 )]
275 #[serde(default)]
276 pub behavior: Option<String>,
277
278 #[schemars(
279 title = "Ссылка на иконку способности",
280 schema_with = "webp_url_schema"
281 )]
282 pub icon_url: String,
283
284 #[schemars(title = "Иконка", schema_with = "asset_ability_icon_schema")]
285 pub icon_path: String,
286
287 #[schemars(title = "Перезарядка способности")]
288 pub cooldown: u64,
289
290 #[schemars(
294 title = "Стоимость каста в мане",
295 description = "Сколько маны списывается за один каст. 0 — способность не тратит ману."
296 )]
297 pub mana_cost: i64,
298
299 #[schemars(title = "Показывается в окне гачи, умеет выпадать из гачи")]
300 pub is_gacha_ability: bool,
301
302 #[schemars(title = "Доступна ботам (для генерации оппонентов)")]
303 #[serde(default)]
304 pub available_to_bots: bool,
305
306 #[schemars(title = "Id редкости", schema_with = "ability_rarity_link_id_schema")]
307 pub rarity_id: AbilityRarityId,
308
309 #[schemars(title = "Описание способности")]
310 pub description: i18n::I18nString,
311
312 #[schemars(
313 title = "Скрипт, возвращающий значения для подстановки в описание",
314 schema_with = "option_script_schema"
315 )]
316 pub description_values_script: Option<String>,
317
318 #[schemars(title = "VFX", schema_with = "asset_vfx_object_schema")]
319 pub vfx_object_path: String,
320
321 #[schemars(title = "Тип каста")]
322 pub cast_type: AbilityCastType,
323
324 #[schemars(title = "Тип цели (Enemy / Ally / Self)")]
325 #[serde(default)]
326 pub target_type: AbilityTargetType,
327
328 #[schemars(title = "Дальность каста (в клетках)")]
329 #[serde(default)]
330 pub range: i64,
331
332 #[schemars(title = "AoE: макс. целей за каст (пусто = без лимита)")]
337 pub max_targets: Option<i64>,
338
339 #[schemars(
343 title = "Теги способности",
344 description = "На них кейтятся законы, камни поддержки (Compatibility) и триггеры. Пустой список — способность без тегов."
345 )]
346 pub tags: Vec<AbilityTag>,
347}
348
349impl AbilityTemplate {
350 pub fn has_tag(&self, tag: AbilityTag) -> bool {
351 self.tags.contains(&tag)
352 }
353
354 pub fn has_any_tag(&self, tags: &[AbilityTag]) -> bool {
355 tags.iter().any(|tag| self.has_tag(*tag))
356 }
357}
358
359#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
360#[tsify(from_wasm_abi)]
361pub struct Ability {
362 pub template_id: AbilityId,
363 pub level: i64,
364 pub shards_amount: i64,
365}
366
367impl Ability {
368 pub fn from_template(
369 ability_template: &AbilityTemplate,
370 level: Option<i64>,
371 shards_amount: Option<i64>,
372 ) -> Self {
373 Ability {
374 template_id: ability_template.id,
375 level: level.unwrap_or(1),
376 shards_amount: shards_amount.unwrap_or(0),
377 }
378 }
379}
380
381#[derive(Clone, Debug, Serialize, Deserialize, Eq, JsonSchema, Tsify)]
382pub struct ActiveAbility {
383 pub ability: Ability,
384 #[cfg_attr(target_arch = "wasm32", schemars(skip))]
385 pub deadline: Option<chrono::DateTime<chrono::Utc>>,
386 pub slot_id: Option<AbilitySlotId>,
387}
388
389impl PartialEq for ActiveAbility {
390 fn eq(&self, other: &Self) -> bool {
391 self.ability == other.ability
392 && self.slot_id == other.slot_id
393 && self.deadline.zip(other.deadline).map_or(
394 self.deadline.is_none() && other.deadline.is_none(),
395 |(a, b)| a.signed_duration_since(b).abs() <= chrono::TimeDelta::milliseconds(250),
396 )
397 }
398}
399
400#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
401pub struct UpgradedAbilitiesMap(pub HashMap<AbilityId, (i64, i64)>);
402
403impl UpgradedAbilitiesMap {
404 pub fn upgrade(&mut self, id: AbilityId, level: i64) {
405 self.0
406 .entry(id)
407 .and_modify(|(_, max)| {
408 *max += 1;
409 })
410 .or_insert((level, level + 1));
411 }
412
413 pub fn insert(&mut self, id: AbilityId, levels: (i64, i64)) {
414 self.0.insert(id, levels);
415 }
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
419pub struct AbilityShard {
420 pub ability_id: AbilityId,
421 pub shards_amount: i64,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
425pub struct AbilityDrop {
426 pub template: AbilityTemplate,
427 pub is_new: bool,
428 pub evolved_from: Option<AbilityTemplate>,
429 pub is_checkpoint: bool,
430}
431
432#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, Default)]
433#[tsify(from_wasm_abi)]
434pub struct EquippedAbilities {
435 pub slotted: BTreeMap<AbilitySlotId, Ability>,
436 pub unslotted: Vec<Ability>,
437}
438
439impl EquippedAbilities {
440 pub fn new() -> Self {
441 Self {
442 slotted: BTreeMap::new(),
443 unslotted: Vec::new(),
444 }
445 }
446
447 pub fn add(&mut self, ability: Ability, slot_id: Option<AbilitySlotId>) {
448 if let Some(slot_id) = slot_id {
449 self.slotted.insert(slot_id, ability);
450 } else {
451 self.unslotted.push(ability);
452 }
453 }
454
455 pub fn to_vec(&self) -> Vec<&Ability> {
456 self.unslotted.iter().chain(self.slotted.values()).collect()
457 }
458
459 pub fn to_vec_mut(&mut self) -> Vec<&mut Ability> {
460 self.unslotted
461 .iter_mut()
462 .chain(self.slotted.values_mut())
463 .collect()
464 }
465
466 pub fn get_by_slot_id(&self, slot_id: AbilitySlotId) -> Option<&Ability> {
467 self.slotted.get(&slot_id)
468 }
469
470 pub fn has_ability(&self, ability_id: AbilityId) -> bool {
471 self.slotted.values().any(|a| a.template_id == ability_id)
472 || self.unslotted.iter().any(|a| a.template_id == ability_id)
473 }
474
475 pub fn get_mut_by_id(&mut self, ability_id: AbilityId) -> Option<&mut Ability> {
476 self.slotted
477 .values_mut()
478 .find(|a| a.template_id == ability_id)
479 .or_else(|| {
480 self.unslotted
481 .iter_mut()
482 .find(|a| a.template_id == ability_id)
483 })
484 }
485
486 pub fn remove_by_slot_id(&mut self, slot_id: AbilitySlotId) -> Option<Ability> {
487 self.slotted.remove(&slot_id)
488 }
489
490 pub fn len(&self) -> usize {
491 self.slotted.len() + self.unslotted.len()
492 }
493
494 pub fn is_empty(&self) -> bool {
495 self.slotted.is_empty() && self.unslotted.is_empty()
496 }
497
498 pub fn clear(&mut self) {
499 self.slotted.clear();
500 self.unslotted.clear();
501 }
502}