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