overlord_event_system/mechanics/
power_q.rs1use configs::game_config::GameConfig;
26use essences::ability_stones::AbilityStoneId;
27use essences::character_state::CharacterState;
28use essences::items::ItemType;
29use essences::stones::{Stone, StoneSocketSlot};
30use essences::{artifacts::ArtifactCollection, cores::CoresState};
31
32use crate::game_config_helpers::GameConfigLookup;
33use crate::mechanics::cores as cores_mech;
34
35pub fn interpolated_q(first_rank: f64, max_rank_value: f64, rank: i64, max_rank: i64) -> f64 {
41 let first = first_rank.max(1.0);
42 let last = max_rank_value.max(first);
43 if max_rank <= 1 {
44 return first;
45 }
46 let t = ((rank.max(1) - 1) as f64 / (max_rank - 1) as f64).clamp(0.0, 1.0);
47 (first.ln() + (last.ln() - first.ln()) * t).exp()
48}
49
50pub fn dynamic_power_multiplier(config: &GameConfig, character: &CharacterState) -> f64 {
56 let mut q = 1.0;
57 q *= laws_and_bridges_q(config, &character.cores);
58 q *= artifacts_q(config, &character.artifacts);
59 q *= ability_stones_q(config, character);
60 q *= pet_facets_q(config, character);
61 q *= equipment_q(config, character);
62 q *= class_q(config, character);
63 q.max(1.0)
64}
65
66fn class_q(config: &GameConfig, character: &CharacterState) -> f64 {
75 const MAX_CLASS_LEVEL: i64 = 20;
76
77 let active = character.character.class;
78 let Some(class) = config.classes.iter().find(|c| c.id == active) else {
79 return 1.0;
80 };
81
82 let class_level = character
85 .character_classes
86 .iter()
87 .find(|row| row.class_id == active)
88 .map(|row| row.level as i64)
89 .unwrap_or(1);
90
91 let mut q = interpolated_q(
92 class.passive_power_q_l1,
93 class.passive_power_q_l20,
94 class_level,
95 MAX_CLASS_LEVEL,
96 );
97
98 for row in &class.ability_power_q {
99 let equipped = character
100 .equipped_abilities
101 .slotted
102 .values()
103 .any(|ability| ability.template_id == row.ability_id);
104 if equipped {
105 q *= row.q.max(1.0);
106 }
107 }
108 q
109}
110
111fn laws_and_bridges_q(config: &GameConfig, cores: &CoresState) -> f64 {
117 let max_rank = config
118 .cores_settings
119 .law_upgrade_ladder
120 .iter()
121 .map(|step| step.level)
122 .max()
123 .unwrap_or(1);
124
125 let law_q = |law_id| -> Option<f64> {
126 let owned = cores
127 .laws
128 .iter()
129 .find(|law| law.template_id == law_id && law.slot_index.is_some())?;
130 let template = cores_mech::law_template(config, law_id)?;
131 Some(interpolated_q(
132 template.power_q_first_rank,
133 template.power_q_max_rank,
134 owned.level,
135 max_rank,
136 ))
137 };
138
139 let mut q = 1.0;
140 for law in cores.laws.iter().filter(|law| law.slot_index.is_some()) {
141 if let Some(law_value) = law_q(law.template_id) {
142 q *= law_value;
143 }
144 }
145
146 let fill = config
149 .game_settings
150 .power_estimator
151 .bridge_expected_fill
152 .clamp(0.0, 1.0);
153 for bridge in &cores.bridges {
154 let (Some(real_q), Some(fantasy_q)) =
155 (law_q(bridge.real_law_id), law_q(bridge.fantasy_law_id))
156 else {
157 continue;
158 };
159 q *= (0.5 * fill * fantasy_q.ln() + 0.5 * fill * real_q.ln()).exp();
160 }
161 q
162}
163
164fn artifacts_q(config: &GameConfig, collection: &ArtifactCollection) -> f64 {
167 let mut q = 1.0;
168
169 if let Some(equipped) = collection.equipped
170 && let Some(template) = config.artifacts.iter().find(|a| a.id == equipped)
171 {
172 q *= template.power_q.max(1.0);
173 }
174
175 let max_rank = config.artifacts_settings.max_stone_level;
176 for stone in collection.stones.iter().filter(|s| s.socket.is_some()) {
177 let Some(template) = config
178 .artifact_stones
179 .iter()
180 .find(|t| t.id == stone.template_id)
181 else {
182 continue;
183 };
184 if !template.active {
185 continue;
186 }
187 q *= interpolated_q(
188 template.power_q_first_rank,
189 template.power_q_max_rank,
190 stone.level,
191 max_rank,
192 );
193 }
194 q
195}
196
197fn ability_stones_q(config: &GameConfig, character: &CharacterState) -> f64 {
201 let max_rank = config.ability_stone_settings.upgrade_copies.len() as i64 + 1;
202 let level_of = |stone_id: AbilityStoneId| {
203 character
204 .ability_stones
205 .iter()
206 .find(|owned| owned.template_id == stone_id)
207 .map_or(1, |owned| owned.level)
208 };
209
210 let mut q = 1.0;
211 for (ability_id, sockets) in &character.ability_stone_sockets.0 {
212 if !character
213 .equipped_abilities
214 .slotted
215 .values()
216 .any(|ability| ability.template_id == *ability_id)
217 {
218 continue;
219 }
220 for stone_id in sockets.values() {
221 let Some(template) = config.ability_stone_template(*stone_id) else {
222 continue;
223 };
224 q *= interpolated_q(
225 template.power_q_first_rank,
226 template.power_q_max_rank,
227 level_of(*stone_id),
228 max_rank,
229 );
230 }
231 }
232 q
233}
234
235fn pet_facets_q(config: &GameConfig, character: &CharacterState) -> f64 {
239 const MAX_PET_LEVEL: i64 = 10;
240
241 let mut real: Vec<f64> = Vec::new();
242 let mut fantasy: Vec<f64> = Vec::new();
243
244 for pet in character.equipped_pets.slotted.values() {
245 let Some(template) = config.pet_template(pet.template_id) else {
246 continue;
247 };
248 let face_q = |facet| {
249 config
250 .pet_facet_settings
251 .power_q_row(facet)
252 .map(|row| {
253 interpolated_q(row.q_first_level, row.q_max_level, pet.level, MAX_PET_LEVEL)
254 })
255 .unwrap_or(1.0)
256 };
257 real.push(face_q(template.real_facet));
258 fantasy.push(face_q(template.fantasy_facet));
259 }
260
261 let mean = |faces: &[f64]| {
262 if faces.is_empty() {
263 1.0
264 } else {
265 faces.iter().sum::<f64>() / faces.len() as f64
266 }
267 };
268 0.5 * mean(&real) + 0.5 * mean(&fantasy)
269}
270
271fn equipment_q(config: &GameConfig, character: &CharacterState) -> f64 {
280 use strum::IntoEnumIterator;
281
282 let settings = &config.game_settings.power_estimator;
283 let reference_rate = settings.reference_trigger_proc_rate.max(f64::MIN_POSITIVE);
284 let max_rank = config.stones_settings.max_stone_level;
285
286 let socketed = |item_type: ItemType, slot: StoneSocketSlot, stones: &[Stone]| {
287 stones
288 .iter()
289 .find(|stone| {
290 stone
291 .socket
292 .is_some_and(|key| key.item_type == item_type && key.socket == slot)
293 })
294 .cloned()
295 };
296
297 let mut q = 1.0;
298 for item_type in ItemType::iter() {
299 let trigger = socketed(
300 item_type,
301 StoneSocketSlot::Trigger,
302 &character.stones.trigger_stones,
303 );
304 let Some(trigger) = trigger else {
305 continue;
306 };
307 let Some(trigger_template) = config.trigger_stone_template(trigger.template_id) else {
308 continue;
309 };
310 if !trigger_template.active {
311 continue;
312 }
313
314 let rate_scale = (trigger_template.power_proc_rate / reference_rate).max(0.0);
315
316 let side_q = |slot: StoneSocketSlot| -> f64 {
317 let Some(effect) = socketed(item_type, slot, &character.stones.effect_stones) else {
318 return 1.0;
319 };
320 let Some(template) = config.effect_stone_template(effect.template_id) else {
321 return 1.0;
322 };
323 let base = interpolated_q(
324 template.power_q_base_first_rank,
325 template.power_q_base_max_rank,
326 effect.level,
327 max_rank,
328 );
329 let tier_scale = config
332 .stones_settings
333 .tier_coefficients
334 .iter()
335 .find(|row| row.trigger_tier == trigger.tier && row.effect_tier == effect.tier)
336 .map_or(1.0, |row| row.multiplier);
337 1.0 + rate_scale * tier_scale * (base - 1.0)
338 };
339
340 let real = side_q(StoneSocketSlot::RealEffect);
341 let fantasy = side_q(StoneSocketSlot::FantasyEffect);
342 let package = ((real.ln() + fantasy.ln()) / 2.0).exp();
343 q *= package.max(settings.trigger_only_q);
344 }
345 q
346}