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(
135    PartialEq, Serialize, Deserialize, Default, Debug, Clone, JsonSchema, Tsify, i18n::Translate,
136)]
137pub struct Class {
138    #[schemars(schema_with = "id_schema")]
139    pub id: ClassId,
140
141    #[schemars(title = "Название класса")]
142    #[translate]
143    pub name: i18n::I18nString,
144
145    #[schemars(title = "Описание класса")]
146    #[translate]
147    pub description: i18n::I18nString,
148
149    #[schemars(title = "Иконка", schema_with = "asset_class_hero_icon")]
150    pub icon_path: String,
151
152    #[schemars(
153        title = "Фон для Hero Screen",
154        schema_with = "asset_class_hero_background"
155    )]
156    pub background_image_path: String,
157
158    #[schemars(title = "Спайн класса", schema_with = "spine_schema")]
159    pub character_asset: String,
160
161    #[schemars(title = "Время каста")]
162    pub cast_time: u64,
163
164    #[schemars(title = "Спайн класса (Unity)", schema_with = "aa_entity_spine_schema")]
165    pub spine: String,
166
167    #[schemars(
168        title = "Базовые способности игрока",
169        description = "Способности, которыми игрок безусловно обладает в бою.",
170        schema_with = "ability_link_id_array_schema"
171    )]
172    pub basic_abilities: Vec<abilities::AbilityId>,
173
174    #[schemars(
175        title = "Активные классовые скиллы",
176        description = "Список активных скиллов класса. Анлокаются и прокачиваются через прокачку класса (ClassLevels.ability_levels), не падают в гаче, экипируются игроком как обычные скиллы. Должны иметь is_gacha_ability=false и не пересекаться с basic_abilities.",
177        schema_with = "ability_link_id_array_schema"
178    )]
179    pub class_abilities: Vec<abilities::AbilityId>,
180
181    #[schemars(
182        title = "Подарочнйый набор новичка",
183        schema_with = "option_bundle_id_schema"
184    )]
185    pub starter_bundle_id: Option<bundles::BundleId>,
186
187    #[schemars(title = "Базовые атрибуты игрока")]
188    pub attributes: Vec<ClassAttribute>,
189
190    #[schemars(
191        title = "Id редкости этого класса",
192        schema_with = "ability_rarity_link_id_schema"
193    )]
194    pub ability_rarity_id: abilities::AbilityRarityId,
195
196    #[schemars(title = "Тир класса")]
197    pub tier: ClassTier,
198
199    #[schemars(
200        title = "Скины для болванки выбора класса",
201        description = "Набор скинов, формирующих внешность \"классического\" представителя класса в экране выбора класса в начале игры",
202        schema_with = "skin_link_id_array_schema"
203    )]
204    pub demo_skins: Vec<SkinId>,
205
206    #[schemars(
207        title = "Тип оружия класса",
208        description = "Отображается в UI; после введения оружейной гачи переедет на enum"
209    )]
210    #[translate]
211    pub weapon_type: i18n::I18nString,
212
213    #[schemars(
214        title = "Пассивка класса",
215        description = "Постоянно действующее правило класса (BAL-033). None — у класса-заглушки."
216    )]
217    pub passive: ClassPassive,
218
219    #[schemars(
220        title = "Множитель Power пассивки на 1 уровне класса",
221        description = "Приблизительный вклад пассивки в displayed Power (BAL-030). 1.0 — не влияет."
222    )]
223    pub passive_power_q_l1: f64,
224
225    #[schemars(
226        title = "Множитель Power пассивки на 20 уровне класса",
227        description = "То же значение на максимальном уровне; промежуточные интерполируются линейно по ln(q)."
228    )]
229    pub passive_power_q_l20: f64,
230
231    #[schemars(
232        title = "Множители Power классовых абилок",
233        description = "По строке на каждую абилку кита; учитывается, только пока абилка экипирована."
234    )]
235    pub ability_power_q: Vec<ClassAbilityPowerQ>,
236
237    #[schemars(
238        title = "Величина пассивки на 1 уровне класса",
239        description = "Проценты: доля MaxHP для лечения, шанс для повтора удара. 0 для None."
240    )]
241    pub passive_percent_l1: f64,
242
243    #[schemars(
244        title = "Величина пассивки на 20 уровне класса",
245        description = "Тот же смысл; уровни между 1 и 20 интерполируются линейно."
246    )]
247    pub passive_percent_l20: f64,
248
249    #[schemars(
250        title = "Период пассивки, тики",
251        description = "Только для периодических пассивок (Priest): через сколько миллисекунд после старта боя и между срабатываниями. 0 у остальных."
252    )]
253    pub passive_period_ticks: u64,
254
255    #[schemars(
256        title = "Основной атрибут класса",
257        description = "Используется в UI блока Class Main Stat",
258        schema_with = "attribute_link_id_schema"
259    )]
260    pub main_attribute: AttributeId,
261
262    #[schemars(
263        title = "Рекомендуемое значение основного атрибута",
264        description = "Отображается в UI как ориентир \"комфортной игры\" на текущий момент"
265    )]
266    pub main_attribute_optimal_value: u64,
267
268    #[schemars(
269        title = "Дефолтный скин оружия класса",
270        description = "Применяется к экипированному оружию после респека, заменяя любой пользовательский скин",
271        schema_with = "skin_link_id_schema"
272    )]
273    pub default_weapon_skin_id: SkinId,
274}