essences/
class.rs

1use super::{abilities, bundles, currency::CurrencyUnit, game};
2use crate::characters::CharacterId;
3use crate::items::AttributeId;
4use crate::skins::SkinId;
5
6use crate::prelude::*;
7
8#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
9pub struct CharacterClass {
10    pub character_id: CharacterId,
11    pub class_id: ClassId,
12    pub level: u64,
13    pub xp: u64,
14}
15
16/// Approximate Power contribution of one class ability while it is EQUIPPED
17/// (BAL-030's `q`). The ability's damage or heal payload is already priced
18/// inside `P_static`; this is the utility half — a damage window, a cooldown
19/// cut, a team buff — that no closed-form stat can express.
20#[derive(PartialEq, Serialize, Deserialize, Default, Debug, Clone, JsonSchema, Tsify)]
21pub struct ClassAbilityPowerQ {
22    #[schemars(title = "Id абилки", schema_with = "ability_link_id_schema")]
23    pub ability_id: abilities::AbilityId,
24
25    #[schemars(
26        title = "Множитель Power",
27        description = "1.0 — утилита абилки уже полностью учтена в статах."
28    )]
29    pub q: f64,
30}
31
32#[derive(PartialEq, Eq, Serialize, Deserialize, Default, Debug, Clone, JsonSchema, Tsify)]
33pub struct ClassAbilityLevel {
34    #[schemars(title = "Id абилки", schema_with = "ability_link_id_schema")]
35    pub ability_id: abilities::AbilityId,
36    #[schemars(title = "Уровень абилки, до которого она прокачивается на этом уровне класса")]
37    pub level: u64,
38}
39
40#[derive(PartialEq, Eq, Serialize, Deserialize, Default, Debug, Clone, JsonSchema, Tsify)]
41pub struct ClassLevels {
42    #[schemars(schema_with = "class_id_schema")]
43    pub class_id: ClassId,
44
45    #[schemars(title = "Уровень класса")]
46    pub level: u64,
47
48    #[schemars(
49        title = "Атрибуты, выдаваемые на этом уровне",
50        description = "Список бонусов к атрибутам, которые игрок получает с этого уровня класса"
51    )]
52    pub attrs: Vec<game::EntityAttribute>,
53
54    #[schemars(
55        title = "Стоимость прокачки",
56        description = "Цена в валюте за прокачку с предыдущего уровня"
57    )]
58    pub price: CurrencyUnit,
59
60    #[schemars(
61        title = "Анлоки/прокачки классовых абилок",
62        description = "Список пар (абилка, уровень), применяемых на этом уровне класса"
63    )]
64    pub ability_levels: Vec<ClassAbilityLevel>,
65}
66use strum_macros::{Display, EnumIter, EnumString};
67
68#[derive(
69    Debug,
70    Clone,
71    Copy,
72    EnumString,
73    Display,
74    Deserialize,
75    Serialize,
76    Hash,
77    Eq,
78    PartialEq,
79    EnumIter,
80    Default,
81    JsonSchema,
82    Tsify,
83)]
84#[tsify(namespace)]
85pub enum ClassTier {
86    #[default]
87    S,
88    A,
89    B,
90    C,
91}
92
93/// The always-on rule a class plays with (BAL-033). One per class: it is the
94/// half of class identity that is not a stat, so it must be readable from the
95/// class alone rather than inferred from what the character happens to wear.
96///
97/// The magnitude interpolates linearly between the two authored endpoints over
98/// global Class Level 1..=20 — a `%MaxHP` heal for `BlockHeal` and
99/// `LowestAllyHeal`, a chance in whole percent for `DoubleStrike`.
100#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Tsify)]
101#[tsify(namespace)]
102pub enum ClassPassive {
103    /// No passive — the unselected sentinel class.
104    #[default]
105    None,
106    /// Every successful Block heals the blocker (Warrior).
107    BlockHeal,
108    /// Every original Basic Attack may repeat itself at full damage (Rogue).
109    DoubleStrike,
110    /// The `multicast_chance` stat IS the passive (Mage): the runtime already
111    /// casts the second copy, and the class level authors the chance.
112    Multicast,
113    /// A periodic heal on the ally with the lowest HP share, self in solo
114    /// (Priest).
115    LowestAllyHeal,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
119pub enum ClassAttribute {
120    EntityAttribute(game::EntityAttribute),
121}
122
123#[declare]
124pub type ClassId = Uuid;
125
126impl ClassPassive {
127    /// Whether this passive needs a fight-local timer.
128    pub const fn is_periodic(self) -> bool {
129        matches!(self, Self::LowestAllyHeal)
130    }
131}
132
133/// Not `Eq`: the passive magnitudes are floats (BAL-033).
134#[derive(PartialEq, Serialize, Deserialize, Default, Debug, Clone, JsonSchema, Tsify)]
135pub struct Class {
136    #[schemars(schema_with = "id_schema")]
137    pub id: ClassId,
138
139    #[schemars(title = "Название класса")]
140    pub name: i18n::I18nString,
141
142    #[schemars(title = "Описание класса")]
143    pub description: i18n::I18nString,
144
145    #[schemars(title = "Иконка", schema_with = "asset_class_hero_icon")]
146    pub icon_path: String,
147
148    #[schemars(
149        title = "Фон для Hero Screen",
150        schema_with = "asset_class_hero_background"
151    )]
152    pub background_image_path: String,
153
154    #[schemars(title = "Спайн класса", schema_with = "spine_schema")]
155    pub character_asset: String,
156
157    #[schemars(title = "Время каста")]
158    pub cast_time: u64,
159
160    #[schemars(title = "Спайн класса (Unity)", schema_with = "aa_entity_spine_schema")]
161    pub spine: String,
162
163    #[schemars(
164        title = "Базовые способности игрока",
165        description = "Способности, которыми игрок безусловно обладает в бою.",
166        schema_with = "ability_link_id_array_schema"
167    )]
168    pub basic_abilities: Vec<abilities::AbilityId>,
169
170    #[schemars(
171        title = "Активные классовые скиллы",
172        description = "Список активных скиллов класса. Анлокаются и прокачиваются через прокачку класса (ClassLevels.ability_levels), не падают в гаче, экипируются игроком как обычные скиллы. Должны иметь is_gacha_ability=false и не пересекаться с basic_abilities.",
173        schema_with = "ability_link_id_array_schema"
174    )]
175    pub class_abilities: Vec<abilities::AbilityId>,
176
177    #[schemars(
178        title = "Подарочнйый набор новичка",
179        schema_with = "option_bundle_id_schema"
180    )]
181    pub starter_bundle_id: Option<bundles::BundleId>,
182
183    #[schemars(title = "Базовые атрибуты игрока")]
184    pub attributes: Vec<ClassAttribute>,
185
186    #[schemars(
187        title = "Id редкости этого класса",
188        schema_with = "ability_rarity_link_id_schema"
189    )]
190    pub ability_rarity_id: abilities::AbilityRarityId,
191
192    #[schemars(title = "Тир класса")]
193    pub tier: ClassTier,
194
195    #[schemars(
196        title = "Скины для болванки выбора класса",
197        description = "Набор скинов, формирующих внешность \"классического\" представителя класса в экране выбора класса в начале игры",
198        schema_with = "skin_link_id_array_schema"
199    )]
200    pub demo_skins: Vec<SkinId>,
201
202    #[schemars(
203        title = "Тип оружия класса",
204        description = "Отображается в UI; после введения оружейной гачи переедет на enum"
205    )]
206    pub weapon_type: i18n::I18nString,
207
208    #[schemars(
209        title = "Пассивка класса",
210        description = "Постоянно действующее правило класса (BAL-033). None — у класса-заглушки."
211    )]
212    pub passive: ClassPassive,
213
214    #[schemars(
215        title = "Множитель Power пассивки на 1 уровне класса",
216        description = "Приблизительный вклад пассивки в displayed Power (BAL-030). 1.0 — не влияет."
217    )]
218    pub passive_power_q_l1: f64,
219
220    #[schemars(
221        title = "Множитель Power пассивки на 20 уровне класса",
222        description = "То же значение на максимальном уровне; промежуточные интерполируются линейно по ln(q)."
223    )]
224    pub passive_power_q_l20: f64,
225
226    #[schemars(
227        title = "Множители Power классовых абилок",
228        description = "По строке на каждую абилку кита; учитывается, только пока абилка экипирована."
229    )]
230    pub ability_power_q: Vec<ClassAbilityPowerQ>,
231
232    #[schemars(
233        title = "Величина пассивки на 1 уровне класса",
234        description = "Проценты: доля MaxHP для лечения, шанс для повтора удара. 0 для None."
235    )]
236    pub passive_percent_l1: f64,
237
238    #[schemars(
239        title = "Величина пассивки на 20 уровне класса",
240        description = "Тот же смысл; уровни между 1 и 20 интерполируются линейно."
241    )]
242    pub passive_percent_l20: f64,
243
244    #[schemars(
245        title = "Период пассивки, тики",
246        description = "Только для периодических пассивок (Priest): через сколько миллисекунд после старта боя и между срабатываниями. 0 у остальных."
247    )]
248    pub passive_period_ticks: u64,
249
250    #[schemars(
251        title = "Основной атрибут класса",
252        description = "Используется в UI блока Class Main Stat",
253        schema_with = "attribute_link_id_schema"
254    )]
255    pub main_attribute: AttributeId,
256
257    #[schemars(
258        title = "Рекомендуемое значение основного атрибута",
259        description = "Отображается в UI как ориентир \"комфортной игры\" на текущий момент"
260    )]
261    pub main_attribute_optimal_value: u64,
262
263    #[schemars(
264        title = "Дефолтный скин оружия класса",
265        description = "Применяется к экипированному оружию после респека, заменяя любой пользовательский скин",
266        schema_with = "skin_link_id_schema"
267    )]
268    pub default_weapon_skin_id: SkinId,
269}