overlord_event_system/mechanics/
ability_stones.rs1use configs::game_config::GameConfig;
10use essences::{
11 abilities::AbilityId,
12 ability_stones::{AbilityStoneMods, AbilityStoneSockets, OwnedAbilityStone},
13 flip::WorldSide,
14};
15
16use crate::game_config_helpers::GameConfigLookup;
17
18#[derive(Clone, Copy, Debug)]
22pub struct AbilityStoneResolver<'a> {
23 sockets: Option<&'a AbilityStoneSockets>,
24 owned: &'a [OwnedAbilityStone],
25 active_side: WorldSide,
26 missing_mana_fraction: f64,
28}
29
30impl Default for AbilityStoneResolver<'_> {
31 fn default() -> Self {
32 Self::none()
33 }
34}
35
36impl<'a> AbilityStoneResolver<'a> {
37 pub const fn none() -> Self {
39 Self {
40 sockets: None,
41 owned: &[],
42 active_side: WorldSide::Fantasy,
43 missing_mana_fraction: 0.0,
44 }
45 }
46
47 pub const fn new(
48 sockets: &'a AbilityStoneSockets,
49 owned: &'a [OwnedAbilityStone],
50 active_side: WorldSide,
51 missing_mana_fraction: f64,
52 ) -> Self {
53 Self {
54 sockets: Some(sockets),
55 owned,
56 active_side,
57 missing_mana_fraction,
58 }
59 }
60
61 pub fn active_side(&self) -> WorldSide {
62 self.active_side
63 }
64
65 pub fn mods_for(
69 &self,
70 config: &GameConfig,
71 ability_id: AbilityId,
72 ability_level: i64,
73 ) -> AbilityStoneMods {
74 let mut mods = AbilityStoneMods::identity();
75 let Some(sockets) = self.sockets else {
76 return mods;
77 };
78
79 let settings = &config.ability_stone_settings;
80 let is_class_ability = config.is_class_ability(ability_id);
81 for (socket_index, stone_id) in sockets.sockets_of(ability_id) {
82 let Some(socket) = settings.socket(is_class_ability, socket_index) else {
83 continue;
84 };
85 if socket.side != self.active_side {
87 continue;
88 }
89 if ability_level < socket.unlock_ability_level {
91 continue;
92 }
93 let Some(template) = config.ability_stone_template(stone_id) else {
94 tracing::error!("Socketed ability stone {stone_id} is not in config");
95 continue;
96 };
97 let level = self
98 .owned
99 .iter()
100 .find(|owned| owned.template_id == stone_id)
101 .map(|owned| owned.level)
102 .unwrap_or(1);
103 for op in &template.ops {
105 mods.apply(op, op.value_at_level(level));
106 }
107 }
108
109 mods.damage_mult = mods.damage_mult_with_mana(self.missing_mana_fraction);
110 mods.missing_mana_damage_per_10pct = 0.0;
111 mods
112 }
113}
114
115pub fn resolver_for_caster<'a>(
123 state: &'a crate::state::OverlordState,
124 caster: &essences::entity::Entity,
125) -> AbilityStoneResolver<'a> {
126 let Some(build) = crate::entities::combatant_character_state_of(state, caster.id) else {
127 return AbilityStoneResolver::none();
128 };
129 let active_side = caster
132 .flip_state
133 .map(|flip| flip.active_side)
134 .unwrap_or(state.flip_state.active_side);
135 let missing_mana_fraction = caster
136 .mana
137 .map(|mana| mana.missing_fraction())
138 .unwrap_or(0.0);
139
140 AbilityStoneResolver::new(
141 &build.ability_stone_sockets,
142 &build.ability_stones,
143 active_side,
144 missing_mana_fraction,
145 )
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use configs::tests_game_config::generate_game_config_for_tests;
152 use essences::ability_stones::AbilityStoneOpKind;
153
154 fn stone_with_single_op(config: &GameConfig, kind: AbilityStoneOpKind) -> uuid::Uuid {
156 config
157 .ability_stones
158 .iter()
159 .find(|s| s.ops.len() == 1 && s.ops[0].kind == kind)
160 .unwrap_or_else(|| panic!("fixture catalog has a single-op {kind} stone"))
161 .id
162 }
163
164 fn sharpening(config: &GameConfig) -> uuid::Uuid {
165 stone_with_single_op(config, AbilityStoneOpKind::PayloadMult)
166 }
167
168 #[test]
169 fn no_sockets_means_identity() {
170 let config = generate_game_config_for_tests();
171 let ability_id = config.abilities[0].id;
172
173 let mods = AbilityStoneResolver::none().mods_for(&config, ability_id, 10);
174
175 assert!(mods.is_identity());
176 }
177
178 #[test]
179 fn only_the_active_side_sockets_apply() {
180 let config = generate_game_config_for_tests();
181 let ability_id = config.abilities[0].id;
182 let stone_id = sharpening(&config);
183 let owned = vec![OwnedAbilityStone::new(stone_id)];
184 let mut sockets = AbilityStoneSockets::default();
185 sockets.set(ability_id, 0, stone_id);
187
188 let real = AbilityStoneResolver::new(&sockets, &owned, WorldSide::Real, 0.0)
189 .mods_for(&config, ability_id, 10);
190 let fantasy = AbilityStoneResolver::new(&sockets, &owned, WorldSide::Fantasy, 0.0)
191 .mods_for(&config, ability_id, 10);
192
193 assert!((real.damage_mult - 1.12).abs() < 1e-9);
194 assert!(fantasy.is_identity());
195 }
196
197 #[test]
198 fn socket_above_ability_level_is_inert() {
199 let config = generate_game_config_for_tests();
200 let ability_id = config.abilities[0].id;
201 let stone_id = sharpening(&config);
202 let owned = vec![OwnedAbilityStone::new(stone_id)];
203 let mut sockets = AbilityStoneSockets::default();
204 sockets.set(ability_id, 2, stone_id); let below = AbilityStoneResolver::new(&sockets, &owned, WorldSide::Real, 0.0)
207 .mods_for(&config, ability_id, 5);
208 let at = AbilityStoneResolver::new(&sockets, &owned, WorldSide::Real, 0.0)
209 .mods_for(&config, ability_id, 6);
210
211 assert!(below.is_identity());
212 assert!((at.damage_mult - 1.12).abs() < 1e-9);
213 }
214
215 #[test]
216 fn stone_level_scales_the_magnitude() {
217 let config = generate_game_config_for_tests();
218 let ability_id = config.abilities[0].id;
219 let stone_id = sharpening(&config);
220 let owned = vec![OwnedAbilityStone {
221 template_id: stone_id,
222 level: 3,
223 copies: 0,
224 }];
225 let mut sockets = AbilityStoneSockets::default();
226 sockets.set(ability_id, 0, stone_id);
227
228 let mods = AbilityStoneResolver::new(&sockets, &owned, WorldSide::Real, 0.0)
229 .mods_for(&config, ability_id, 10);
230
231 assert!((mods.damage_mult - 1.24).abs() < 1e-9);
233 }
234
235 #[test]
236 fn missing_mana_stone_is_resolved_into_damage_mult() {
237 let config = generate_game_config_for_tests();
238 let ability_id = config.abilities[0].id;
239 let stone_id = stone_with_single_op(&config, AbilityStoneOpKind::MissingManaDamagePercent);
240 let owned = vec![OwnedAbilityStone::new(stone_id)];
241 let mut sockets = AbilityStoneSockets::default();
242 sockets.set(ability_id, 0, stone_id);
243
244 let mods = AbilityStoneResolver::new(&sockets, &owned, WorldSide::Real, 0.5)
245 .mods_for(&config, ability_id, 10);
246
247 assert!((mods.damage_mult - 1.4).abs() < 1e-9);
248 assert_eq!(mods.missing_mana_damage_per_10pct, 0.0);
249 }
250}