1use super::{abilities, artifacts, currency, items};
2
3use tsify_next::Tsify;
4
5use crate::prelude::*;
6use strum_macros::{Display, EnumIter, EnumString};
7
8#[declare]
9pub type BundleId = uuid::Uuid;
10
11#[derive(
12 Debug,
13 Clone,
14 Copy,
15 EnumString,
16 Display,
17 Deserialize,
18 Serialize,
19 Hash,
20 Eq,
21 PartialEq,
22 EnumIter,
23 Default,
24 JsonSchema,
25 Tsify,
26)]
27#[tsify(namespace)]
28pub enum BundleStepType {
29 #[default]
30 Currency,
31 Ability,
32 Item,
33 Artifact,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
40pub struct BundleRawStep {
41 #[schemars(title = "Тип элемента в бандле'")]
42 pub item_type: BundleStepType,
43
44 #[schemars(
45 title = "Валюты (фиксированная награда)",
46 description = "Фиксированная валютная награда шага (для шагов типа Currency). \
47 Единый источник правды для сервера и клиента — переносит хардкод значений из \
48 нативных fixed_currencies fn в конфиг. Если непусто, используется вместо `script`."
49 )]
50 #[serde(default)]
51 pub currencies: Vec<currency::CurrencyUnit>,
52
53 #[schemars(
54 title = "Валюты по условию",
55 description = "Валютная награда, выбираемая по custom_values персонажа (для шагов \
56 типа Currency): ветки проверяются по порядку, первая совпавшая выдаёт свои \
57 валюты, иначе — default. Если задано, используется вместо `script`."
58 )]
59 #[serde(default)]
60 pub currency_branch: Option<CurrencyBranchStep>,
61
62 #[schemars(
63 title = "Нативная функция валют",
64 description = "Имя нативной функции категории currencies, вычисляющей валюты \
65 (для шагов типа Currency). Используется только для НЕ-фиксированных наград \
66 (afk-начисление); фиксированные награды берутся из `currencies`, выбор по \
67 состоянию — из `currency_branch`.",
68 schema_with = "currencies_ref_schema"
69 )]
70 #[serde(default)]
71 pub behavior: Option<String>,
72
73 #[schemars(
74 title = "Осколки способностей (фиксированная награда)",
75 description = "Фиксированный список осколков способностей шага (для шагов типа \
76 Ability)."
77 )]
78 #[serde(default)]
79 pub shards: Vec<BundleShardAmount>,
80
81 #[schemars(
82 title = "Предметы (фиксированная награда)",
83 description = "Фиксированный список template_id предметов шага (для шагов типа \
84 Item).",
85 schema_with = "item_link_id_array_schema"
86 )]
87 #[serde(default)]
88 pub item_template_ids: Vec<items::ItemTemplateId>,
89
90 #[schemars(
91 title = "Артефакты (фиксированная награда)",
92 description = "Фиксированный список template_id артефактов шага (для шагов типа \
93 Artifact). Повторная выдача уже имеющегося артефакта копится как сырая копия \
94 для его прокачки — так и задумано для повторной победы в топ-1.",
95 schema_with = "artifact_link_id_array_schema"
96 )]
97 #[serde(default)]
98 pub artifact_template_ids: Vec<artifacts::ArtifactTemplateId>,
99
100 #[schemars(
101 title = "TTL предметов в секундах",
102 description = "Если задано, выданные этим шагом предметы временные: после \
103 истечения срока они удаляются на старте следующего боя. null — постоянные \
104 предметы. Применяется только к шагам типа Item."
105 )]
106 #[serde(default)]
107 pub item_ttl_seconds: Option<i64>,
108
109 #[schemars(title = "Нужно ли показывать попап")]
110 pub has_pop_up: bool,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
115pub struct BundleShardAmount {
116 #[schemars(title = "Способность", schema_with = "ability_link_id_schema")]
117 pub ability_id: abilities::AbilityId,
118 #[schemars(title = "Количество осколков")]
119 pub amount: i64,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
125pub struct CurrencyBranchStep {
126 #[schemars(title = "Ветки (по порядку)")]
127 pub branches: Vec<CurrencyBranch>,
128 #[schemars(title = "Валюты по умолчанию")]
129 pub default: Vec<currency::CurrencyUnit>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
134pub struct CurrencyBranch {
135 #[schemars(title = "Условие")]
136 pub condition: CustomValueCondition,
137 #[schemars(title = "Валюты")]
138 pub currencies: Vec<currency::CurrencyUnit>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
145pub enum CustomValueCondition {
146 #[schemars(title = "Ключ не установлен")]
147 KeyUnset { key: String },
148 #[schemars(title = "Ключ равен значению")]
149 KeyEquals { key: String, value: i64 },
150 #[schemars(title = "Значения двух ключей равны")]
151 KeysEqual { left: String, right: String },
152}
153
154impl CurrencyBranchStep {
155 pub fn evaluate(
158 &self,
159 character: &crate::character_state::CharacterState,
160 ) -> &Vec<currency::CurrencyUnit> {
161 let value = |key: &str| character.character.custom_values.0.get(key).copied();
162 for branch in &self.branches {
163 let matches = match &branch.condition {
164 CustomValueCondition::KeyUnset { key } => value(key).is_none(),
165 CustomValueCondition::KeyEquals { key, value: v } => value(key) == Some(*v),
166 CustomValueCondition::KeysEqual { left, right } => value(left) == value(right),
167 };
168 if matches {
169 return &branch.currencies;
170 }
171 }
172 &self.default
173 }
174}
175
176#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Default, JsonSchema, Tsify)]
177#[tsify(namespace)]
178pub enum BundleClaimMode {
179 #[default]
180 Sequential,
181 AllAtOnce,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
185pub struct BundleRaw {
186 #[schemars(schema_with = "id_schema")]
187 pub id: BundleId,
188
189 #[schemars(title = "Содержимое бандла")]
190 pub steps: Vec<BundleRawStep>,
191
192 #[schemars(title = "Режим получения наград", default = "default_claim_mode")]
193 #[serde(default)]
194 pub claim_mode: BundleClaimMode,
195}
196
197fn default_claim_mode() -> BundleClaimMode {
198 BundleClaimMode::Sequential
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Tsify, Default)]
202pub struct BundleAbility {
203 pub template: abilities::AbilityTemplate,
204 pub shards_amount: i64,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Tsify)]
208#[tsify(into_wasm_abi)]
209pub enum BundleElement {
210 Currencies(Vec<currency::CurrencyUnit>),
211 Abilities(Vec<BundleAbility>),
212 Items(Vec<items::Item>),
213 Artifacts(Vec<artifacts::ArtifactTemplateId>),
218}
219
220impl Default for BundleElement {
221 fn default() -> Self {
222 BundleElement::Currencies(Vec::new())
223 }
224}
225
226#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify, Default)]
227pub struct BundleStep {
228 pub bundle_id: BundleId,
229 pub element: BundleElement,
230 pub has_pop_up: bool,
231 #[serde(skip)]
242 pub source: currency::CurrencySource,
243}
244
245#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
246#[tsify(into_wasm_abi)]
247pub enum BundleStepGeneric {
248 Sequential(BundleStep),
249 AllAtOnce {
250 bundle_id: BundleId,
251 elements: Vec<BundleElement>,
252 has_pop_up: bool,
253 #[serde(skip)]
255 source: currency::CurrencySource,
256 },
257}
258
259impl Default for BundleStepGeneric {
260 fn default() -> Self {
261 BundleStepGeneric::Sequential(BundleStep::default())
262 }
263}
264
265impl BundleStepGeneric {
266 pub fn has_pop_up(&self) -> bool {
267 match self {
268 BundleStepGeneric::Sequential(step) => step.has_pop_up,
269 BundleStepGeneric::AllAtOnce { has_pop_up, .. } => *has_pop_up,
270 }
271 }
272
273 pub fn bundle_id(&self) -> BundleId {
274 match self {
275 BundleStepGeneric::Sequential(step) => step.bundle_id,
276 BundleStepGeneric::AllAtOnce { bundle_id, .. } => *bundle_id,
277 }
278 }
279
280 pub fn source(&self) -> currency::CurrencySource {
281 match self {
282 BundleStepGeneric::Sequential(step) => step.source,
283 BundleStepGeneric::AllAtOnce { source, .. } => *source,
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use currency::CurrencySource;
292 use uuid::uuid;
293
294 const BUNDLE_ID: BundleId = uuid!("00000000-0000-0000-0000-000000000001");
295
296 #[test]
297 fn source_accessor_returns_sequential_step_source() {
298 let step = BundleStepGeneric::Sequential(BundleStep {
299 bundle_id: BUNDLE_ID,
300 element: BundleElement::default(),
301 has_pop_up: false,
302 source: CurrencySource::QuestClaim,
303 });
304 assert_eq!(step.source(), CurrencySource::QuestClaim);
305 }
306
307 #[test]
308 fn source_accessor_returns_all_at_once_source() {
309 let step = BundleStepGeneric::AllAtOnce {
310 bundle_id: BUNDLE_ID,
311 elements: vec![BundleElement::default()],
312 has_pop_up: false,
313 source: CurrencySource::MailReward,
314 };
315 assert_eq!(step.source(), CurrencySource::MailReward);
316 }
317
318 #[test]
322 fn missing_source_in_serialized_step_defaults_to_bundle_claim() {
323 let json = format!(
324 r#"{{"bundle_id":"{BUNDLE_ID}","element":{{"Currencies":[]}},"has_pop_up":true}}"#
325 );
326 let step: BundleStep = serde_json::from_str(&json).unwrap();
327 assert_eq!(step.source, CurrencySource::BundleClaim);
328 }
329
330 #[test]
331 fn missing_source_in_all_at_once_defaults_to_bundle_claim() {
332 let json = format!(
333 r#"{{"AllAtOnce":{{"bundle_id":"{BUNDLE_ID}","elements":[],"has_pop_up":false}}}}"#
334 );
335 let generic: BundleStepGeneric = serde_json::from_str(&json).unwrap();
336 assert_eq!(generic.source(), CurrencySource::BundleClaim);
337 }
338
339 #[test]
346 fn source_does_not_round_trip_through_serde() {
347 let original = BundleStepGeneric::Sequential(BundleStep {
348 bundle_id: BUNDLE_ID,
349 element: BundleElement::default(),
350 has_pop_up: true,
351 source: CurrencySource::OfferBuy,
352 });
353 let json = serde_json::to_string(&original).unwrap();
354 assert!(
357 !json.contains("source"),
358 "serialized form leaked source field: {json}"
359 );
360 let round_tripped: BundleStepGeneric = serde_json::from_str(&json).unwrap();
361 assert_eq!(round_tripped.source(), CurrencySource::BundleClaim);
362 }
363
364 #[test]
368 fn new_currency_sources_serialize_as_bundle_claim_for_wire_compat() {
369 let new_variants = [
370 CurrencySource::MailReward,
371 CurrencySource::DungeonReward,
372 CurrencySource::OfferBuy,
373 CurrencySource::ChapterReward,
374 CurrencySource::NewUserGrant,
375 CurrencySource::PvpArenaReward,
376 CurrencySource::PvpVassalReward,
377 CurrencySource::AfkReward,
378 ];
379 for v in new_variants {
380 let json = serde_json::to_string(&v).unwrap();
381 assert_eq!(json, "\"BundleClaim\"", "{v:?} leaked over serde");
382 }
383
384 assert_eq!(
386 serde_json::to_string(&CurrencySource::QuestClaim).unwrap(),
387 "\"QuestClaim\"",
388 );
389 }
390}