1use configs::game_config::GameConfig;
2use essences::{
3 bundles::{BundleAbility, BundleElement, BundleRawStep, BundleStepType},
4 character_state::CharacterState,
5 currency::from_es_currencies,
6 items::Item,
7};
8
9use event_system::script::random::GameRng;
10
11use crate::{
12 BehaviorRegistry, cases::try_finalize_item, gacha::item_case::generate_item_from_template,
13 game_config_helpers::GameConfigLookup,
14};
15use rand::SeedableRng;
16
17pub fn bundle_raw_step_to_element(
18 raw_item: &BundleRawStep,
19 character_state: &CharacterState,
20 behaviors: &BehaviorRegistry,
21 game_config: &GameConfig,
22) -> BundleElement {
23 match raw_item.item_type {
24 BundleStepType::Currency => {
25 process_currency(raw_item, character_state, behaviors, game_config)
26 }
27 BundleStepType::Ability => {
28 process_ability(raw_item, character_state, behaviors, game_config)
29 }
30 BundleStepType::Item => process_item(raw_item, character_state, behaviors, game_config),
31 }
32}
33
34pub fn bundle_raw_afk_step_to_element(
35 raw_item: &BundleRawStep,
36 character_state: &CharacterState,
37 now: chrono::DateTime<chrono::Utc>,
38 behaviors: &BehaviorRegistry,
39 game_config: &GameConfig,
40) -> BundleElement {
41 match raw_item.item_type {
42 BundleStepType::Currency => {
43 process_afk_currency(raw_item, character_state, now, behaviors, game_config)
44 }
45 BundleStepType::Ability => {
46 process_afk_ability(raw_item, character_state, now, behaviors, game_config)
47 }
48 BundleStepType::Item => {
49 process_afk_item(raw_item, character_state, now, behaviors, game_config)
50 }
51 }
52}
53
54fn process_currency(
57 raw_item: &BundleRawStep,
58 character_state: &CharacterState,
59 behaviors: &BehaviorRegistry,
60 game_config: &GameConfig,
61) -> BundleElement {
62 if !raw_item.currencies.is_empty() {
66 return BundleElement::Currencies(raw_item.currencies.clone());
67 }
68 if let Some(branch) = &raw_item.currency_branch {
69 return BundleElement::Currencies(branch.evaluate(character_state).clone());
70 }
71
72 let es_currencies = raw_item
73 .behavior
74 .as_deref()
75 .and_then(|name| behaviors.currencies_fn(name))
76 .map(|f| {
77 f(&crate::behaviors::rewards::RewardCtx {
78 character: Some(character_state),
79 last_claim_at: None,
80 now: None,
81 rng: None,
82 config: game_config,
83 lookups: behaviors.lookups(),
84 })
85 })
86 .transpose()
87 .unwrap_or_else(|e| {
88 tracing::error!("Failed to run native currencies bundle step: {e}");
89 None
90 })
91 .unwrap_or_default();
92
93 BundleElement::Currencies(from_es_currencies(&es_currencies))
94}
95
96fn process_afk_currency(
100 raw_item: &BundleRawStep,
101 character_state: &CharacterState,
102 now: chrono::DateTime<chrono::Utc>,
103 behaviors: &BehaviorRegistry,
104 game_config: &GameConfig,
105) -> BundleElement {
106 let last_claim_at = character_state
107 .character
108 .last_afk_reward_claimed_at
109 .timestamp()
110 .max(0) as u64;
111 let now_secs = now.timestamp().max(0) as u64;
112 let afk_seed = character_state.character.afk_reward_seed;
113 let rng = GameRng::new(rand::rngs::StdRng::seed_from_u64(afk_seed));
114
115 let es_currencies = raw_item
116 .behavior
117 .as_deref()
118 .and_then(|name| behaviors.currencies_fn(name))
119 .map(|f| {
120 f(&crate::behaviors::rewards::RewardCtx {
121 character: Some(character_state),
122 last_claim_at: Some(last_claim_at),
123 now: Some(now_secs),
124 rng: Some(&rng),
125 config: game_config,
126 lookups: behaviors.lookups(),
127 })
128 })
129 .transpose()
130 .unwrap_or_else(|e| {
131 tracing::error!("Failed to run native afk currencies bundle step: {e}");
132 None
133 })
134 .unwrap_or_default();
135
136 BundleElement::Currencies(from_es_currencies(&es_currencies))
137}
138
139fn process_ability(
141 raw_item: &BundleRawStep,
142 _character_state: &CharacterState,
143 _script_runner: &BehaviorRegistry,
144 game_config: &GameConfig,
145) -> BundleElement {
146 BundleElement::Abilities(build_abilities(&raw_item.shards, game_config))
147}
148
149fn process_afk_ability(
151 raw_item: &BundleRawStep,
152 _character_state: &CharacterState,
153 _now: chrono::DateTime<chrono::Utc>,
154 _script_runner: &BehaviorRegistry,
155 game_config: &GameConfig,
156) -> BundleElement {
157 BundleElement::Abilities(build_abilities(&raw_item.shards, game_config))
158}
159
160fn build_abilities(
161 shards: &[essences::bundles::BundleShardAmount],
162 game_config: &GameConfig,
163) -> Vec<BundleAbility> {
164 shards
165 .iter()
166 .filter_map(|shard| {
167 let Some(template) = game_config.ability_template(shard.ability_id).cloned() else {
168 tracing::error!("Failed to get ability with ability_id={}", shard.ability_id);
169 return None;
170 };
171
172 Some(BundleAbility {
173 template,
174 shards_amount: shard.amount,
175 })
176 })
177 .collect()
178}
179
180fn process_item(
181 raw_item: &BundleRawStep,
182 character_state: &CharacterState,
183 behaviors: &BehaviorRegistry,
184 game_config: &GameConfig,
185) -> BundleElement {
186 let items: Vec<Item> = raw_item
187 .item_template_ids
188 .iter()
189 .filter_map(|&item_id| {
190 let Some(template) = game_config.item_template(item_id) else {
191 tracing::error!("Failed to get item template with item_id={}", item_id);
192 return None;
193 };
194
195 let Some(rarity) = game_config.item_rarity(template.rarity_id).cloned() else {
196 tracing::error!("Failed to get item rarity with id={}", template.rarity_id);
197 return None;
198 };
199
200 Some(generate_item_from_template(
201 template,
202 rarity,
203 character_state.character.character_level,
204 game_config,
205 &mut rand::rngs::StdRng::from_os_rng(),
206 ))
207 })
208 .collect();
209
210 let expires_at = item_ttl_expires_at(raw_item, ::time::utc_now());
211 let finalized_items = items
212 .into_iter()
213 .filter_map(
214 |mut item| match try_finalize_item(&mut item, game_config, behaviors) {
215 Ok(()) => {
216 item.expires_at = expires_at;
217 Some(item)
218 }
219 Err(e) => {
220 tracing::error!("Failed to finalize item: {}", e);
221 None
222 }
223 },
224 )
225 .collect();
226
227 BundleElement::Items(finalized_items)
228}
229
230fn item_ttl_expires_at(
233 raw_item: &BundleRawStep,
234 now: chrono::DateTime<chrono::Utc>,
235) -> Option<chrono::DateTime<chrono::Utc>> {
236 raw_item
237 .item_ttl_seconds
238 .map(|secs| now + chrono::Duration::seconds(secs))
239}
240
241fn process_afk_item(
242 raw_item: &BundleRawStep,
243 character_state: &CharacterState,
244 now: chrono::DateTime<chrono::Utc>,
245 behaviors: &BehaviorRegistry,
246 game_config: &GameConfig,
247) -> BundleElement {
248 let items: Vec<Item> = raw_item
249 .item_template_ids
250 .iter()
251 .filter_map(|&item_id| {
252 let Some(template) = game_config.item_template(item_id) else {
253 tracing::error!("Failed to get item template with item_id={}", item_id);
254 return None;
255 };
256
257 let Some(rarity) = game_config.item_rarity(template.rarity_id).cloned() else {
258 tracing::error!("Failed to get item rarity with id={}", template.rarity_id);
259 return None;
260 };
261
262 Some(generate_item_from_template(
263 template,
264 rarity,
265 character_state.character.character_level,
266 game_config,
267 &mut rand::rngs::StdRng::seed_from_u64(character_state.character.afk_reward_seed),
268 ))
269 })
270 .collect();
271
272 let expires_at = item_ttl_expires_at(raw_item, now);
273 let finalized_items = items
274 .into_iter()
275 .filter_map(
276 |mut item| match try_finalize_item(&mut item, game_config, behaviors) {
277 Ok(()) => {
278 item.expires_at = expires_at;
279 Some(item)
280 }
281 Err(e) => {
282 tracing::error!("Failed to finalize item: {}", e);
283 None
284 }
285 },
286 )
287 .collect();
288
289 BundleElement::Items(finalized_items)
290}