overlord_event_system/behaviors/
rewards.rs1use configs::afk_rewards::AfkRewardBonusType;
7use configs::game_config::GameConfig;
8use essences::character_state::CharacterState;
9use event_system::script::types::ESCurrencyUnit;
10use uuid::Uuid;
11
12use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
13use crate::mechanics::content_lookups::ContentLookups;
14
15pub struct RewardCtx<'a> {
19 pub character: Option<&'a CharacterState>,
20 pub last_claim_at: Option<u64>,
22 pub now: Option<u64>,
24 pub rng: Option<&'a event_system::script::random::GameRng>,
26 pub config: &'a GameConfig,
27 pub lookups: &'a ContentLookups,
28}
29
30pub type RewardFn = fn(&RewardCtx) -> anyhow::Result<Vec<ESCurrencyUnit>>;
33
34pub(crate) fn fixed_currencies(entries: &[(&str, i64)]) -> anyhow::Result<Vec<ESCurrencyUnit>> {
37 entries
38 .iter()
39 .map(|(id, amount)| {
40 let currency_id = Uuid::parse_str(id)
41 .map_err(|err| anyhow::anyhow!("rewards: bad uuid {id:?}: {err}"))?;
42 Ok(ESCurrencyUnit {
43 currency_id,
44 amount: *amount,
45 })
46 })
47 .collect()
48}
49
50pub fn afk_rewards_step0(ctx: &RewardCtx) -> anyhow::Result<Vec<ESCurrencyUnit>> {
60 use std::collections::BTreeMap;
61
62 let character = ctx
63 .character
64 .ok_or_else(|| anyhow::anyhow!("afk_rewards_step0: CharacterState not in scope"))?;
65 let now = ctx
66 .now
67 .ok_or_else(|| anyhow::anyhow!("afk_rewards_step0: Now not in scope"))?;
68 let last_claim_at = ctx
69 .last_claim_at
70 .ok_or_else(|| anyhow::anyhow!("afk_rewards_step0: LastClaimAt not in scope"))?;
71 let rng = ctx
72 .rng
73 .ok_or_else(|| anyhow::anyhow!("afk_rewards_step0: Random not in scope"))?;
74
75 let duration_talent_id = Uuid::from_u128(0x019d1c46_af25_741f_a514_9175dda443b2);
77 let efficiency_talent_id = Uuid::from_u128(0x019d1c46_fd2e_7b36_b7e3_51b7fcd8553c);
78 let duration_talent_level = character.talent_levels.0.get(&duration_talent_id).copied();
79 let efficiency_talent_level = character
80 .talent_levels
81 .0
82 .get(&efficiency_talent_id)
83 .copied();
84
85 let settings = &ctx.config.afk_rewards_settings;
86
87 let mut max_duration = settings.max_possible_time_sec as f64;
88 if let Some(level) = duration_talent_level {
89 max_duration *= 1.0 + 0.1 * level as f64;
90 }
91
92 let mut efficiency = 1.0_f64;
93 if let Some(level) = efficiency_talent_level {
94 efficiency *= 1.0 + 0.1 * level as f64;
95 }
96
97 let current_chapter_level = character.character.current_chapter_level;
104 let ads_multiplier = character.character.afk_boost_pending_stacks + 1;
105
106 let mut afk_levels: Vec<&_> = ctx.config.afk_rewards_levels.iter().collect();
108 afk_levels.sort_by_key(|l| std::cmp::Reverse(l.chapter_level));
109 let Some(level) = afk_levels
110 .into_iter()
111 .find(|l| l.chapter_level <= current_chapter_level)
112 else {
113 return Ok(vec![]);
114 };
115
116 let delta_secs = (now as i64).wrapping_sub(last_claim_at as i64);
117 let seconds = (delta_secs as f64).min(max_duration);
118
119 let minutes = seconds / 60.0;
120 let bonus_iterations =
121 (seconds / settings.bonus_calculation_rate_sec.get() as f64 * efficiency).floor() as i64;
122
123 let mut res: BTreeMap<String, i64> = BTreeMap::new();
124
125 for currency_rate in &level.currency_rates {
126 let per_min: f64 = currency_rate.rate_per_minute.get();
127 let amount = (per_min * minutes).floor() as i64 * ads_multiplier;
128 let key = currency_rate.currency_id.to_string();
129 *res.entry(key).or_insert(0) += amount;
130 }
131
132 if !level.bonus_weights.is_empty() {
133 for _ in 0..bonus_iterations {
134 if let Some(bonus) = rand_weight_bonus(rng, &level.bonus_weights)
135 && let AfkRewardBonusType::Currency(currency_id) = &bonus.bonus_type
136 {
137 let amount = bonus.count.get();
138 *res.entry(currency_id.to_string()).or_insert(0) += amount;
139 }
140 }
141 }
142
143 res.into_iter()
144 .map(|(k, amount)| {
145 let currency_id = Uuid::parse_str(&k)
146 .map_err(|err| anyhow::anyhow!("afk_rewards_step0: bad uuid {k:?}: {err}"))?;
147 Ok(ESCurrencyUnit {
148 currency_id,
149 amount,
150 })
151 })
152 .collect()
153}
154
155fn rand_weight_bonus<'a>(
161 rng: &event_system::script::random::GameRng,
162 arr: &'a [configs::afk_rewards::AfkRewardBonusWeight],
163) -> Option<&'a configs::afk_rewards::AfkRewardBonusWeight> {
164 let sum_weight: f64 = arr.iter().map(|e| e.weight.get()).sum();
165 if sum_weight <= 0.0 {
166 return None;
167 }
168 let mut weight = rng.random_f64() * sum_weight;
169 for elem in arr {
170 weight -= elem.weight.get();
171 if weight <= 0.0 {
172 return Some(elem);
173 }
174 }
175 arr.last()
176}
177
178pub fn test_afk_currency_step0(ctx: &RewardCtx) -> anyhow::Result<Vec<ESCurrencyUnit>> {
181 let now = ctx
182 .now
183 .ok_or_else(|| anyhow::anyhow!("test_afk_currency_step0: Now not in scope"))?;
184 let last_claim_at = ctx
185 .last_claim_at
186 .ok_or_else(|| anyhow::anyhow!("test_afk_currency_step0: LastClaimAt not in scope"))?;
187 let amount = if now.saturating_sub(last_claim_at) > 8 {
188 228
189 } else {
190 111
191 };
192 fixed_currencies(&[("b59b33a2-4d19-4e2c-9cea-e03ea15882a0", amount)])
193}
194
195pub fn register(registry: &mut BehaviorRegistry) {
197 registry.register_currencies(
198 BehaviorMeta {
199 name: "afk_rewards_step0".to_string(),
200 category: BehaviorKind::Currencies,
201 title: "AFK-начисление валют".to_string(),
202 description: "Начисление валют за оффлайн-время: ставки в минуту с учётом \
203 талантов и ads-буста, плюс взвешенные бонусные дропы \
204 (детерминированный RNG от afk_reward_seed)."
205 .to_string(),
206 },
207 afk_rewards_step0,
208 );
209 registry.register_currencies(
210 BehaviorMeta {
211 name: "test_afk_currency_step0".to_string(),
212 category: BehaviorKind::Currencies,
213 title: "Тест: AFK-награда (111 / 228)".to_string(),
214 description: "Тестовый AFK-шаг: Now - LastClaimAt > 8 → 228, иначе 111 мягкой валюты."
215 .to_string(),
216 },
217 test_afk_currency_step0,
218 );
219}