overlord_event_system/behaviors/
opponents.rs1use configs::game_config::GameConfig;
24use essences::abilities::AbilityTemplate;
25use essences::item_case::InventoryLevel;
26use essences::items::{ItemRarity, ItemTemplate};
27use event_system::script::random::GameRng;
28use serde::Serialize;
29use uuid::Uuid;
30
31use crate::mechanics::balance;
32use crate::mechanics::content_lookups::ContentLookups;
33
34pub struct OpponentGenCtx<'a> {
37 pub expected_rating: i64,
38 pub rng: &'a GameRng,
39 pub config: &'a GameConfig,
40 pub lookups: &'a ContentLookups,
41}
42
43pub type OpponentGenFn = fn(&OpponentGenCtx) -> anyhow::Result<OpponentGenerationResult>;
45
46const MEAN_RATING_FOR_WIN: f64 = 10.0;
47const MEAN_RATING_FOR_LOSS: f64 = -4.0;
48const MEAN_WINS_PER_DAY: f64 = 4.0;
49const MEAN_LOSSES_PER_DAY: f64 = 1.0;
50const START_RATING: f64 = 1000.0;
51const BASIC_ABILITY: u128 = 0x0194d64e_20f2_75e5_89c8_4cb812672485;
53const BOT_CLASS: u128 = 0x0195c7de_144f_7b53_b370_468e9ae8f744;
55
56pub fn default_opponent_generation(
58 ctx: &OpponentGenCtx,
59) -> anyhow::Result<OpponentGenerationResult> {
60 let cfg = ctx.config;
61 let mut result = OpponentGenerationResult::default();
62
63 let mean_rating_per_day =
64 MEAN_WINS_PER_DAY * MEAN_RATING_FOR_WIN + MEAN_LOSSES_PER_DAY * MEAN_RATING_FOR_LOSS;
65 let rating_delta = ctx.expected_rating as f64 - START_RATING;
66 let day = (rating_delta / mean_rating_per_day).max(0.0);
67 let level = balance::rand_round_f64(balance::level_by_day(day), ctx.rng); let item_q = balance::item_q_by_day(day);
69
70 let mut inv: Vec<&InventoryLevel> = cfg.inventory_levels.iter().collect();
73 inv.sort_by_key(|l| std::cmp::Reverse(l.from_chapter_level));
74 let Some(current_inventory_level) = inv.into_iter().find(|l| l.from_chapter_level <= level)
75 else {
76 anyhow::bail!("opponent_generation: no inventory level for level {level}");
77 };
78
79 let mut items: Vec<&ItemTemplate> = cfg.items.iter().collect();
81 items.sort_by_key(|a| a.id.to_string());
82 let mut item_rarities: Vec<&ItemRarity> = cfg.item_rarities.iter().collect();
83 item_rarities.sort_by_key(|a| a.id.to_string());
84
85 for item_type in ¤t_inventory_level.item_types {
86 let q_code = balance::rand_round_f64(item_q, ctx.rng); let Some(item_rarity) = item_rarities.iter().find(|q| q.code == q_code) else {
88 continue;
89 };
90 if let Some(item) = items
91 .iter()
92 .find(|i| i.item_type == *item_type && i.rarity_id == item_rarity.id)
93 {
94 result.push_item(UuidIntPair {
95 id: item.id,
96 value: level,
97 });
98 }
99 }
100
101 let character_level = cfg
106 .character_levels
107 .iter()
108 .find(|c| c.level == level)
109 .or_else(|| cfg.character_levels.iter().max_by_key(|c| c.level));
110 let mut ability_slots = character_level.map(|c| c.ability_slots as i64).unwrap_or(0);
111
112 result.push_ability(UuidIntPair {
113 id: Uuid::from_u128(BASIC_ABILITY),
114 value: 1,
115 });
116 let spell_power = balance::eff_spell_by_level(level as f64);
117
118 if ability_slots > 0 {
119 let mut abilities: Vec<&AbilityTemplate> = cfg
120 .abilities
121 .iter()
122 .filter(|a| a.available_to_bots)
123 .collect();
124 abilities.sort_by_key(|a| a.id.to_string());
125 while ability_slots > 0 && !abilities.is_empty() {
126 let len = abilities.len();
128 let idx = ((ctx.rng.random_f64() * len as f64).floor() as usize).min(len - 1);
129 let ability = abilities.remove(idx);
130 ability_slots -= 1;
131 let rarity_eff = ctx
132 .lookups
133 .ability_rarity_eff
134 .get(&ability.rarity_id)
135 .copied()
136 .unwrap_or(1.0);
137 let level_dps = (spell_power / rarity_eff - 0.4).max(1.0);
138 let ability_level = ((level_dps - 1.0) * 100.0).floor() as i64 + 1;
139 result.push_ability(UuidIntPair {
140 id: ability.id,
141 value: ability_level,
142 });
143 }
144 }
145
146 let power_dec_factor = (current_inventory_level.item_types.len() as f64 / 10.0).powf(2.0);
147 let power = 2.0_f64.powf(item_q - 1.0)
148 * balance::eff_by_level(level as f64)
149 * (ability_slots as f64 + 1.0)
150 * spell_power
151 * balance::BASE_POWER as f64
152 * power_dec_factor;
153 result.set_level(level);
154 result.set_class_id(Uuid::from_u128(BOT_CLASS));
155 result.set_power(power.floor() as i64);
156 Ok(result)
157}
158
159#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
160pub struct UuidIntPair {
161 pub id: Uuid,
162 pub value: i64,
163}
164
165#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
166pub struct OpponentGenerationResult {
167 pub items: Vec<UuidIntPair>,
168 pub abilities: Vec<UuidIntPair>,
169 pub power: i64,
170 pub level: i64,
171 pub class_id: Uuid,
172}
173
174impl OpponentGenerationResult {
175 pub fn push_item(&mut self, item: UuidIntPair) {
176 self.items.push(item);
177 }
178
179 pub fn push_ability(&mut self, ability: UuidIntPair) {
180 self.abilities.push(ability);
181 }
182
183 pub fn set_power(&mut self, power: i64) {
184 self.power = power;
185 }
186
187 pub fn set_level(&mut self, level: i64) {
188 self.level = level;
189 }
190
191 pub fn set_class_id(&mut self, class_id: Uuid) {
192 self.class_id = class_id;
193 }
194}