overlord_event_system/
bundles.rs

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
54/// Bundle `currencies` step: fixed list, custom-values branch, or the
55/// config-named native fn, in that order. Empty result if none is set.
56fn process_currency(
57    raw_item: &BundleRawStep,
58    character_state: &CharacterState,
59    behaviors: &BehaviorRegistry,
60    game_config: &GameConfig,
61) -> BundleElement {
62    // Fixed and branch rewards live in the config (single source of truth for
63    // server + client); only non-fixed steps (afk accrual) fall back to a
64    // native `currencies` fn.
65    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
96/// Native afk `currencies` step. The afk port draws against a `StdRng` seeded
97/// from `afk_reward_seed` (via `util::rand_weight`) and reads the
98/// `LastClaimAt`/`Now` constants.
99fn 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
139/// Bundle `ability_shards` step: the typed `shards` list from the config.
140fn 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
149/// Afk `ability_shards` step — same typed `shards` list as the regular path.
150fn 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
230/// Момент истечения временных предметов шага: `now + item_ttl_seconds`.
231/// `None`, если у шага нет TTL (предметы постоянные).
232fn 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}