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(
211 Default, PartialEq, Debug, Clone, Serialize, Deserialize, Tsify, JsonSchema, i18n::Translate,
212)]
213pub struct AbilityRarity {
214 #[schemars(schema_with = "id_schema")]
215 pub id: AbilityRarityId,
216
217 #[schemars(title = "Название редкости")]
218 #[translate]
219 pub name: i18n::I18nString,
220
221 #[schemars(title = "Сортировка")]
222 pub order: u64,
223
224 #[schemars(title = "Цвет редкости", schema_with = "color_schema")]
225 pub color: String,
226
227 #[schemars(title = "Цвет заднего фона редкости", schema_with = "color_schema")]
228 pub bg_color: String,
229
230 #[schemars(title = "Иконка рамки", schema_with = "webp_url_schema")]
231 pub icon_url: String,
232
233 #[schemars(title = "Рамка", schema_with = "asset_ability_rarity_icon_schema")]
234 pub icon_path: String,
235
236 #[schemars(
237 title = "Квадратная рамка",
238 schema_with = "asset_ability_rarity_square_icon_schema"
239 )]
240 pub square_icon_path: String,
241
242 #[schemars(
243 title = "Эффективность редкости (eff)",
244 description = "Множитель урона способностей этой редкости (ability_rarity_eff). Используется в balance::ability_eff."
245 )]
246 #[serde(default)]
247 pub eff: f64,
248}
249
250#[derive(
251 Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema, Default, i18n::Translate,
252)]
253#[tsify(from_wasm_abi)]
254pub struct AbilityTemplate {
255 #[schemars(schema_with = "id_schema")]
256 pub id: AbilityId,
257
258 #[schemars(title = "Видимость в UI боя")]
259 pub is_fight_ui_visible: bool,
260
261 #[schemars(title = "Тип видимости в UI боя")]
262 pub fight_ui_visibility: AbilityFightUiVisibility,
263
264 #[schemars(title = "Имя способности")]
265 #[translate]
266 pub name: i18n::I18nString,
267
268 #[schemars(
269 title = "Нативная функция старта каста",
270 description = "Имя нативной функции категории start_cast_ability, выполняющей старт каста способности.",
271 schema_with = "start_cast_ability_ref_schema"
272 )]
273 #[serde(default)]
274 pub start_behavior: Option<String>,
275
276 #[schemars(
277 title = "Нативная функция каста способности",
278 description = "Имя нативной функции категории cast_ability, выполняющей каст способности.",
279 schema_with = "cast_ability_ref_schema"
280 )]
281 #[serde(default)]
282 pub behavior: Option<String>,
283
284 #[schemars(
285 title = "Ссылка на иконку способности",
286 schema_with = "webp_url_schema"
287 )]
288 pub icon_url: String,
289
290 #[schemars(title = "Иконка", schema_with = "asset_ability_icon_schema")]
291 pub icon_path: String,
292
293 #[schemars(title = "Перезарядка способности")]
294 pub cooldown: u64,
295
296 #[schemars(
300 title = "Стоимость каста в мане",
301 description = "Сколько маны списывается за один каст. 0 — способность не тратит ману."
302 )]
303 pub mana_cost: i64,
304
305 #[schemars(title = "Показывается в окне гачи, умеет выпадать из гачи")]
306 pub is_gacha_ability: bool,
307
308 #[schemars(title = "Доступна ботам (для генерации оппонентов)")]
309 #[serde(default)]
310 pub available_to_bots: bool,
311
312 #[schemars(title = "Id редкости", schema_with = "ability_rarity_link_id_schema")]
313 pub rarity_id: AbilityRarityId,
314
315 #[schemars(title = "Описание способности")]
316 #[translate]
317 pub description: i18n::I18nString,
318
319 #[schemars(
320 title = "Скрипт, возвращающий значения для подстановки в описание",
321 schema_with = "option_script_schema"
322 )]
323 pub description_values_script: Option<String>,
324
325 #[schemars(title = "VFX", schema_with = "asset_vfx_object_schema")]
326 pub vfx_object_path: String,
327
328 #[schemars(title = "Тип каста")]
329 pub cast_type: AbilityCastType,
330
331 #[schemars(title = "Тип цели (Enemy / Ally / Self)")]
332 #[serde(default)]
333 pub target_type: AbilityTargetType,
334
335 #[schemars(title = "Дальность каста (в клетках)")]
336 #[serde(default)]
337 pub range: i64,
338
339 #[schemars(title = "AoE: макс. целей за каст (пусто = без лимита)")]
344 pub max_targets: Option<i64>,
345
346 #[schemars(
350 title = "Теги способности",
351 description = "На них кейтятся законы, камни поддержки (Compatibility) и триггеры. Пустой список — способность без тегов."
352 )]
353 pub tags: Vec<AbilityTag>,
354}
355
356impl AbilityTemplate {
357 pub fn has_tag(&self, tag: AbilityTag) -> bool {
358 self.tags.contains(&tag)
359 }
360
361 pub fn has_any_tag(&self, tags: &[AbilityTag]) -> bool {
362 tags.iter().any(|tag| self.has_tag(*tag))
363 }
364}
365
366#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
367#[tsify(from_wasm_abi)]
368pub struct Ability {
369 pub template_id: AbilityId,
370 pub level: i64,
371 pub shards_amount: i64,
372}
373
374impl Ability {
375 pub fn from_template(
376 ability_template: &AbilityTemplate,
377 level: Option<i64>,
378 shards_amount: Option<i64>,
379 ) -> Self {
380 Ability {
381 template_id: ability_template.id,
382 level: level.unwrap_or(1),
383 shards_amount: shards_amount.unwrap_or(0),
384 }
385 }
386}
387
388#[derive(Clone, Debug, Serialize, Deserialize, Eq, JsonSchema, Tsify)]
389pub struct ActiveAbility {
390 pub ability: Ability,
391 #[cfg_attr(target_arch = "wasm32", schemars(skip))]
392 pub deadline: Option<chrono::DateTime<chrono::Utc>>,
393 pub slot_id: Option<AbilitySlotId>,
394}
395
396impl PartialEq for ActiveAbility {
397 fn eq(&self, other: &Self) -> bool {
398 self.ability == other.ability
399 && self.slot_id == other.slot_id
400 && self.deadline.zip(other.deadline).map_or(
401 self.deadline.is_none() && other.deadline.is_none(),
402 |(a, b)| a.signed_duration_since(b).abs() <= chrono::TimeDelta::milliseconds(250),
403 )
404 }
405}
406
407#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
408pub struct UpgradedAbilitiesMap(pub HashMap<AbilityId, (i64, i64)>);
409
410impl UpgradedAbilitiesMap {
411 pub fn upgrade(&mut self, id: AbilityId, level: i64) {
412 self.0
413 .entry(id)
414 .and_modify(|(_, max)| {
415 *max += 1;
416 })
417 .or_insert((level, level + 1));
418 }
419
420 pub fn insert(&mut self, id: AbilityId, levels: (i64, i64)) {
421 self.0.insert(id, levels);
422 }
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
426pub struct AbilityShard {
427 pub ability_id: AbilityId,
428 pub shards_amount: i64,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
432pub struct AbilityDrop {
433 pub template: AbilityTemplate,
434 pub is_new: bool,
435 pub evolved_from: Option<AbilityTemplate>,
436 pub is_checkpoint: bool,
437}
438
439#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, Default)]
440#[tsify(from_wasm_abi)]
441pub struct EquippedAbilities {
442 pub slotted: BTreeMap<AbilitySlotId, Ability>,
443 pub unslotted: Vec<Ability>,
444}
445
446impl EquippedAbilities {
447 pub fn new() -> Self {
448 Self {
449 slotted: BTreeMap::new(),
450 unslotted: Vec::new(),
451 }
452 }
453
454 pub fn add(&mut self, ability: Ability, slot_id: Option<AbilitySlotId>) {
455 if let Some(slot_id) = slot_id {
456 self.slotted.insert(slot_id, ability);
457 } else {
458 self.unslotted.push(ability);
459 }
460 }
461
462 pub fn to_vec(&self) -> Vec<&Ability> {
463 self.unslotted.iter().chain(self.slotted.values()).collect()
464 }
465
466 pub fn to_vec_mut(&mut self) -> Vec<&mut Ability> {
467 self.unslotted
468 .iter_mut()
469 .chain(self.slotted.values_mut())
470 .collect()
471 }
472
473 pub fn get_by_slot_id(&self, slot_id: AbilitySlotId) -> Option<&Ability> {
474 self.slotted.get(&slot_id)
475 }
476
477 pub fn has_ability(&self, ability_id: AbilityId) -> bool {
478 self.slotted.values().any(|a| a.template_id == ability_id)
479 || self.unslotted.iter().any(|a| a.template_id == ability_id)
480 }
481
482 pub fn get_mut_by_id(&mut self, ability_id: AbilityId) -> Option<&mut Ability> {
483 self.slotted
484 .values_mut()
485 .find(|a| a.template_id == ability_id)
486 .or_else(|| {
487 self.unslotted
488 .iter_mut()
489 .find(|a| a.template_id == ability_id)
490 })
491 }
492
493 pub fn remove_by_slot_id(&mut self, slot_id: AbilitySlotId) -> Option<Ability> {
494 self.slotted.remove(&slot_id)
495 }
496
497 pub fn len(&self) -> usize {
498 self.slotted.len() + self.unslotted.len()
499 }
500
501 pub fn is_empty(&self) -> bool {
502 self.slotted.is_empty() && self.unslotted.is_empty()
503 }
504
505 pub fn clear(&mut self) {
506 self.slotted.clear();
507 self.unslotted.clear();
508 }
509}