1use super::{abilities, 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}
34
35#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
36pub struct BundleRawStep {
37 #[schemars(title = "Тип элемента в бандле'")]
38 pub item_type: BundleStepType,
39
40 #[schemars(title = "Скрипт для элемента", schema_with = "script_schema")]
41 pub script: String,
42
43 #[schemars(title = "Нужно ли показывать попап")]
44 pub has_pop_up: bool,
45}
46
47#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Default, JsonSchema, Tsify)]
48#[tsify(namespace)]
49pub enum BundleClaimMode {
50 #[default]
51 Sequential,
52 AllAtOnce,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Tsify, JsonSchema)]
56pub struct BundleRaw {
57 #[schemars(schema_with = "id_schema")]
58 pub id: BundleId,
59
60 #[schemars(title = "Содержимое бандла")]
61 pub steps: Vec<BundleRawStep>,
62
63 #[schemars(title = "Режим получения наград", default = "default_claim_mode")]
64 #[serde(default)]
65 pub claim_mode: BundleClaimMode,
66}
67
68fn default_claim_mode() -> BundleClaimMode {
69 BundleClaimMode::Sequential
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Tsify, Default)]
73pub struct BundleAbility {
74 pub template: abilities::AbilityTemplate,
75 pub shards_amount: i64,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Tsify)]
79#[tsify(into_wasm_abi)]
80pub enum BundleElement {
81 Currencies(Vec<currency::CurrencyUnit>),
82 Abilities(Vec<BundleAbility>),
83 Items(Vec<items::Item>),
84}
85
86impl Default for BundleElement {
87 fn default() -> Self {
88 BundleElement::Currencies(Vec::new())
89 }
90}
91
92#[derive(
93 Clone, Debug, Serialize, Deserialize, PartialEq, Eq, CustomType, JsonSchema, Tsify, Default,
94)]
95pub struct BundleStep {
96 pub bundle_id: BundleId,
97 pub element: BundleElement,
98 pub has_pop_up: bool,
99 #[serde(skip)]
110 pub source: currency::CurrencySource,
111}
112
113#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
114#[tsify(into_wasm_abi)]
115pub enum BundleStepGeneric {
116 Sequential(BundleStep),
117 AllAtOnce {
118 bundle_id: BundleId,
119 elements: Vec<BundleElement>,
120 has_pop_up: bool,
121 #[serde(skip)]
123 source: currency::CurrencySource,
124 },
125}
126
127impl Default for BundleStepGeneric {
128 fn default() -> Self {
129 BundleStepGeneric::Sequential(BundleStep::default())
130 }
131}
132
133impl BundleStepGeneric {
134 pub fn has_pop_up(&self) -> bool {
135 match self {
136 BundleStepGeneric::Sequential(step) => step.has_pop_up,
137 BundleStepGeneric::AllAtOnce { has_pop_up, .. } => *has_pop_up,
138 }
139 }
140
141 pub fn bundle_id(&self) -> BundleId {
142 match self {
143 BundleStepGeneric::Sequential(step) => step.bundle_id,
144 BundleStepGeneric::AllAtOnce { bundle_id, .. } => *bundle_id,
145 }
146 }
147
148 pub fn source(&self) -> currency::CurrencySource {
149 match self {
150 BundleStepGeneric::Sequential(step) => step.source,
151 BundleStepGeneric::AllAtOnce { source, .. } => *source,
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use currency::CurrencySource;
160 use uuid::uuid;
161
162 const BUNDLE_ID: BundleId = uuid!("00000000-0000-0000-0000-000000000001");
163
164 #[test]
165 fn source_accessor_returns_sequential_step_source() {
166 let step = BundleStepGeneric::Sequential(BundleStep {
167 bundle_id: BUNDLE_ID,
168 element: BundleElement::default(),
169 has_pop_up: false,
170 source: CurrencySource::QuestClaim,
171 });
172 assert_eq!(step.source(), CurrencySource::QuestClaim);
173 }
174
175 #[test]
176 fn source_accessor_returns_all_at_once_source() {
177 let step = BundleStepGeneric::AllAtOnce {
178 bundle_id: BUNDLE_ID,
179 elements: vec![BundleElement::default()],
180 has_pop_up: false,
181 source: CurrencySource::MailReward,
182 };
183 assert_eq!(step.source(), CurrencySource::MailReward);
184 }
185
186 #[test]
190 fn missing_source_in_serialized_step_defaults_to_bundle_claim() {
191 let json = format!(
192 r#"{{"bundle_id":"{BUNDLE_ID}","element":{{"Currencies":[]}},"has_pop_up":true}}"#
193 );
194 let step: BundleStep = serde_json::from_str(&json).unwrap();
195 assert_eq!(step.source, CurrencySource::BundleClaim);
196 }
197
198 #[test]
199 fn missing_source_in_all_at_once_defaults_to_bundle_claim() {
200 let json = format!(
201 r#"{{"AllAtOnce":{{"bundle_id":"{BUNDLE_ID}","elements":[],"has_pop_up":false}}}}"#
202 );
203 let generic: BundleStepGeneric = serde_json::from_str(&json).unwrap();
204 assert_eq!(generic.source(), CurrencySource::BundleClaim);
205 }
206
207 #[test]
214 fn source_does_not_round_trip_through_serde() {
215 let original = BundleStepGeneric::Sequential(BundleStep {
216 bundle_id: BUNDLE_ID,
217 element: BundleElement::default(),
218 has_pop_up: true,
219 source: CurrencySource::OfferBuy,
220 });
221 let json = serde_json::to_string(&original).unwrap();
222 assert!(
225 !json.contains("source"),
226 "serialized form leaked source field: {json}"
227 );
228 let round_tripped: BundleStepGeneric = serde_json::from_str(&json).unwrap();
229 assert_eq!(round_tripped.source(), CurrencySource::BundleClaim);
230 }
231
232 #[test]
236 fn new_currency_sources_serialize_as_bundle_claim_for_wire_compat() {
237 let new_variants = [
238 CurrencySource::MailReward,
239 CurrencySource::DungeonReward,
240 CurrencySource::OfferBuy,
241 CurrencySource::ChapterReward,
242 CurrencySource::NewUserGrant,
243 CurrencySource::PvpArenaReward,
244 CurrencySource::PvpVassalReward,
245 CurrencySource::AfkReward,
246 ];
247 for v in new_variants {
248 let json = serde_json::to_string(&v).unwrap();
249 assert_eq!(json, "\"BundleClaim\"", "{v:?} leaked over serde");
250 }
251
252 assert_eq!(
254 serde_json::to_string(&CurrencySource::QuestClaim).unwrap(),
255 "\"QuestClaim\"",
256 );
257 }
258}