1use crate::prelude::*;
2use chrono::{DateTime, Utc};
3use enum_iterator::Sequence;
4use strum_macros::{Display, EnumIter, EnumString};
5
6use crate::class::ClassId;
7use crate::currency::CurrencyUnit;
8pub use crate::flip::WorldSide;
9use crate::skins::SkinId;
10
11#[declare]
12pub type AttributeId = Uuid;
13
14#[derive(
15 PartialEq,
16 Eq,
17 Default,
18 Hash,
19 Debug,
20 Clone,
21 Deserialize,
22 Serialize,
23 Tsify,
24 JsonSchema,
25 i18n::Translate,
26)]
27pub struct Attribute {
28 #[schemars(schema_with = "id_schema")]
29 pub id: AttributeId,
30
31 #[schemars(title = "Название атрибута")]
32 #[translate]
33 pub name: i18n::I18nString,
34
35 #[schemars(title = "Префикс аттрибута")]
36 #[translate]
37 pub prefix: Option<i18n::I18nString>,
38
39 #[schemars(title = "Суффикс атрибута")]
40 #[translate]
41 pub suffix: Option<i18n::I18nString>,
42
43 #[schemars(title = "Код атрибута")]
44 pub code: String,
45
46 #[schemars(title = "Иконка аттрибута", schema_with = "webp_url_schema")]
47 pub icon: String,
48
49 #[schemars(title = "Иконка", schema_with = "asset_attribute_icon_schema")]
50 pub icon_path: String,
51
52 #[schemars(title = "Технический идентификатор", range(min = 0, max = 63))]
53 pub db_code: u8,
54
55 #[schemars(title = "Делитель значения аттрибута")]
56 pub denominator: Option<i64>,
57
58 #[schemars(title = "Является ли значение процентом")]
59 pub is_percent: bool,
60
61 #[schemars(
62 title = "Нативная функция вычисления значения",
63 description = "Имя нативной функции категории item_attribute, вычисляющей значение атрибута.",
64 schema_with = "item_attribute_ref_schema"
65 )]
66 #[serde(default)]
67 pub calculation_behavior: Option<String>,
68
69 #[schemars(
70 title = "Приоритет атрибута",
71 description = "Приоритет атрибута, чем меньше значение - тем выше приоритет"
72 )]
73 pub order: i64,
74
75 #[schemars(title = "Показывать ли атрибут в ui")]
76 pub is_ui_visible: bool,
77
78 #[schemars(title = "Описание аттрибута")]
79 #[translate]
80 pub description: i18n::I18nString,
81
82 #[schemars(
83 title = "Базовое значение атрибута",
84 description = "Стартовое значение стата до бонусов (например received_damage = 10000 = 100%). \
85 Применяется в get_entity_stat. null = нет базового значения."
86 )]
87 #[serde(default)]
88 pub base_value: Option<i64>,
89}
90
91#[derive(
92 Debug,
93 Clone,
94 Copy,
95 EnumString,
96 Sequence,
97 Display,
98 Deserialize,
99 Serialize,
100 Hash,
101 Eq,
102 PartialEq,
103 EnumIter,
104 Default,
105 JsonSchema,
106 Tsify,
107)]
108#[tsify(namespace)]
109pub enum ItemType {
110 #[default]
111 Weapon,
112 Torso,
113 Head,
114 Gloves,
115 Shoulders,
116 Boots,
117 Legs,
118 Waist,
119 Neck,
120 Ring,
121}
122
123impl ItemType {
124 pub const fn supports_world_side(self) -> bool {
126 matches!(
127 self,
128 Self::Weapon | Self::Torso | Self::Head | Self::Shoulders | Self::Gloves
129 )
130 }
131}
132
133#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
139pub struct EquipmentSlotKey {
140 item_type: ItemType,
141 world_side: Option<WorldSide>,
142}
143
144impl EquipmentSlotKey {
145 pub const fn new(item_type: ItemType, world_side: Option<WorldSide>) -> Self {
146 Self {
147 item_type,
148 world_side: if item_type.supports_world_side() {
149 world_side
150 } else {
151 None
152 },
153 }
154 }
155
156 pub const fn item_type(self) -> ItemType {
157 self.item_type
158 }
159
160 pub const fn world_side(self) -> Option<WorldSide> {
161 self.world_side
162 }
163}
164
165#[declare]
166pub type ItemRarityId = Uuid;
167
168#[derive(
169 Default,
170 PartialEq,
171 Eq,
172 Hash,
173 Debug,
174 Clone,
175 Serialize,
176 Deserialize,
177 Tsify,
178 JsonSchema,
179 i18n::Translate,
180)]
181pub struct ItemRarity {
182 #[schemars(schema_with = "id_schema")]
183 pub id: ItemRarityId,
184 #[schemars(title = "Код редкости (числовой тир, для генерации оппонентов)")]
185 #[serde(default)]
186 pub code: i64,
187 #[schemars(title = "Название редкости")]
188 #[translate]
189 pub name: i18n::I18nString,
190 #[schemars(title = "Цвет редкости текста", schema_with = "color_schema")]
191 pub text_color: String,
192 #[schemars(title = "URL картинки редкости", schema_with = "webp_url_schema")]
193 pub rarity_icon: String,
194 #[schemars(title = "Картинка", schema_with = "asset_item_rarity_icon_schema")]
195 pub rarity_icon_path: String,
196 #[schemars(
197 title = "URL картинки ленточки под айтемом",
198 schema_with = "webp_url_schema"
199 )]
200 pub ribbon_icon: String,
201 #[schemars(
202 title = "Лента-подложка",
203 schema_with = "asset_item_ribbon_icon_schema"
204 )]
205 pub ribbon_icon_path: String,
206 #[schemars(
207 title = "Приоритет редкости",
208 description = "Приоритет редкости, чем меньше значение - тем выше приоритет"
209 )]
210 pub order: i64,
211
212 #[schemars(
213 title = "Качество редкости (q)",
214 description = "Множитель силы предметов этой редкости (item_rarity_q). Используется в balance::eff_item."
215 )]
216 #[serde(default)]
217 pub q: i64,
218
219 #[schemars(
220 title = "Цена продажи (sell_q)",
221 description = "Множитель Gold при продаже предмета этой редкости. Отделён от q намеренно: \
222 q задаёт силу и растёт плавно, а цена продажи должна следовать лестнице цен \
223 сундука, чтобы продажи закрывали 78-82% стоимости следующего апгрейда. \
224 Одно число не может делать и то и другое. Используется только в item_price."
225 )]
226 pub sell_q: i64,
227}
228
229#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Tsify)]
230pub struct ItemAttributeSettings {
231 #[schemars(title = "Количество опциональных атрибутов у элемента")]
232 pub optional_attributes_count: u64,
233
234 #[schemars(
235 title = "Идентификаторы опциональных атрибутов",
236 schema_with = "attribute_link_id_array_schema"
237 )]
238 pub optional_attributes_ids: Vec<AttributeId>,
239}
240
241#[declare]
242pub type ItemTemplateId = Uuid;
243
244#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, Tsify, i18n::Translate)]
245pub struct ItemTemplate {
246 #[schemars(schema_with = "id_schema")]
247 pub id: ItemTemplateId,
248
249 #[schemars(title = "Название предмета")]
250 #[translate]
251 pub name: i18n::I18nString,
252
253 #[schemars(title = "URL картинки", schema_with = "webp_url_schema")]
254 pub icon_url: String,
255
256 #[schemars(title = "Иконка", schema_with = "asset_item_icon_schema")]
257 pub icon_path: String,
258
259 #[schemars(title = "Тип предмета")]
260 pub item_type: ItemType,
261
262 pub world_side: Option<WorldSide>,
264
265 #[schemars(title = "Id редкости", schema_with = "item_rarity_link_id_schema")]
266 pub rarity_id: ItemRarityId,
267
268 #[schemars(title = "Настройки атрибутов для предмета")]
269 pub attributes_settings: ItemAttributeSettings,
270
271 #[schemars(title = "Id скина", schema_with = "option_skin_link_id_schema")]
272 pub skin_id: Option<SkinId>,
273
274 #[schemars(title = "Исключить из выдачи мимика")]
275 pub exclude_from_mimic: bool,
276
277 #[schemars(
278 title = "Требуемый класс",
279 description = "Если задан, предмет может быть получен только игроком с этим классом"
280 )]
281 pub required_class: Option<ClassId>,
282
283 #[schemars(
284 title = "Фиксированная сила предмета",
285 description = "Если задано, заменяет случайный разброс силы (item_fixed_power) в balance::attr_spread_for_item."
286 )]
287 #[serde(default)]
288 pub fixed_power: Option<f64>,
289
290 #[schemars(
291 title = "Код предмета для мимика",
292 description = "Если задан, квест может выдать именно этот предмет из сундука: SetCustomValue(next_mimic_item_code, <код>) у персонажа резолвится в этот шаблон через content::get_item_by_code (item_id_by_mimic_code)."
293 )]
294 #[serde(default)]
295 pub next_mimic_item_code: Option<i64>,
296}
297
298impl ItemTemplate {
299 pub const fn equipment_slot_key(&self) -> EquipmentSlotKey {
300 EquipmentSlotKey::new(self.item_type, self.world_side)
301 }
302}
303
304#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Hash, JsonSchema, Tsify)]
305pub struct ItemAttribute {
306 pub attr_id: AttributeId,
307 pub value: i32,
308}
309
310#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, Default, Hash, JsonSchema, Tsify)]
311#[tsify(from_wasm_abi)]
312pub struct Item {
313 pub id: Uuid,
314 pub item_template_id: ItemTemplateId,
315 pub item_type: ItemType,
316 pub world_side: Option<WorldSide>,
318 pub rarity: ItemRarity,
319 pub level: i64,
320 pub name: i18n::I18nString,
321 pub icon_url: String,
322 pub icon_path: String,
323 pub is_equipped: bool,
324 pub price: Vec<CurrencyUnit>,
325 pub experience: i64,
326 pub attributes: Vec<ItemAttribute>,
327
328 pub power_bonus: i32,
336
337 pub expires_at: Option<DateTime<Utc>>,
340}
341
342impl Item {
343 pub const fn equipment_slot_key(&self) -> EquipmentSlotKey {
344 EquipmentSlotKey::new(self.item_type, self.world_side)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::{EquipmentSlotKey, ItemType};
351 use crate::flip::WorldSide;
352
353 #[test]
354 fn world_side_is_supported_only_by_the_five_flip_types() {
355 for item_type in [
356 ItemType::Weapon,
357 ItemType::Torso,
358 ItemType::Head,
359 ItemType::Shoulders,
360 ItemType::Gloves,
361 ] {
362 assert!(item_type.supports_world_side(), "{item_type} must flip");
363 }
364
365 for item_type in [
366 ItemType::Legs,
367 ItemType::Boots,
368 ItemType::Waist,
369 ItemType::Neck,
370 ItemType::Ring,
371 ] {
372 assert!(
373 !item_type.supports_world_side(),
374 "{item_type} must remain fixed"
375 );
376 }
377 }
378
379 #[test]
380 fn head_and_gloves_keep_side_while_legs_normalize_to_one_slot() {
381 assert_ne!(
382 EquipmentSlotKey::new(ItemType::Head, Some(WorldSide::Real)),
383 EquipmentSlotKey::new(ItemType::Head, Some(WorldSide::Fantasy))
384 );
385 assert_ne!(
386 EquipmentSlotKey::new(ItemType::Gloves, Some(WorldSide::Real)),
387 EquipmentSlotKey::new(ItemType::Gloves, Some(WorldSide::Fantasy))
388 );
389 assert_eq!(
390 EquipmentSlotKey::new(ItemType::Legs, Some(WorldSide::Real)),
391 EquipmentSlotKey::new(ItemType::Legs, Some(WorldSide::Fantasy))
392 );
393 assert_eq!(
394 EquipmentSlotKey::new(ItemType::Legs, Some(WorldSide::Real)).world_side(),
395 None
396 );
397 }
398}