overlord_event_system/mechanics/
support_shapes.rs1use essences::ability_stones::AbilityStoneMods;
12use essences::entity::Entity;
13use essences::fight_breakdown::CombatSource;
14use essences::fighting::ActiveFight;
15use event_system::script::random::GameRng;
16use uuid::Uuid;
17
18use crate::mechanics::content_lookups::ContentLookups;
19use crate::mechanics::effect_cb::OverlordEffectCb;
20use crate::mechanics::fight::{AttackParams, FightSink, attack, heal_entity};
21
22pub struct SupportAttackCtx<'a> {
25 pub rng: &'a GameRng,
26 pub lookups: &'a ContentLookups,
27 pub fight: &'a ActiveFight,
28 pub player_id: Uuid,
29 pub caster: &'a Entity,
30 pub mods: &'a AbilityStoneMods,
31 pub source: CombatSource,
36}
37
38fn extra_targets<'a>(
41 fight: &'a ActiveFight,
42 caster: &Entity,
43 primary: &Entity,
44 count: i64,
45) -> Vec<&'a Entity> {
46 fight
47 .entities
48 .iter()
49 .filter(|e| e.team != caster.team && e.id != primary.id && e.hp > 0)
50 .take(count.max(0) as usize)
51 .collect()
52}
53
54fn derived_params(base: &AttackParams, payload: f64) -> AttackParams {
66 AttackParams {
67 power: base.power.map(|p| p * payload),
68 dot_power: base.dot_power.map(|p| p * payload),
69 no_counterattack: true,
70 derived: true,
71 ..base.clone()
72 }
73}
74
75pub fn attack_with_supports(
83 sink: &mut dyn FightSink,
84 ctx: &SupportAttackCtx,
85 target: &Entity,
86 params: &AttackParams,
87) -> anyhow::Result<Option<i64>> {
88 let mods = ctx.mods;
89 let mut primary = params.clone();
90 primary.crit_chance_bonus += mods.crit_chance_bonus * 10_000.0;
91
92 let run =
93 |sink: &mut dyn FightSink, target: &Entity, params: &AttackParams, source: CombatSource| {
94 let mut effects = OverlordEffectCb;
95 attack(
96 sink,
97 ctx.rng,
98 ctx.lookups,
99 &mut effects,
100 ctx.player_id,
101 ctx.caster,
102 target,
103 params,
104 source,
105 )
106 .map_err(|e| anyhow::anyhow!("attack: {e}"))
107 };
108 let derived_source = ctx.source.derived();
109
110 let primary_damage = run(sink, target, &primary, ctx.source)?;
111 let mut total = primary_damage.unwrap_or(0);
112
113 if mods.split.is_active() {
115 for _ in 0..mods.split.count {
116 let copy = derived_params(&primary, mods.split.payload);
117 total += run(sink, target, ©, derived_source)?.unwrap_or(0);
118 }
119 }
120
121 if mods.chain.is_active() {
123 for jump_target in extra_targets(ctx.fight, ctx.caster, target, mods.chain.count) {
124 let copy = derived_params(&primary, mods.chain.payload);
125 total += run(sink, jump_target, ©, derived_source)?.unwrap_or(0);
126 }
127 }
128
129 if mods.pierce.is_active() {
132 let mut payload = 1.0;
133 for pierced in extra_targets(ctx.fight, ctx.caster, target, mods.pierce.count) {
134 payload *= mods.pierce.payload;
135 let copy = derived_params(&primary, payload);
136 total += run(sink, pierced, ©, derived_source)?.unwrap_or(0);
137 }
138 }
139
140 if mods.leech_fraction > 0.0 && total > 0 {
143 heal_entity(
144 sink,
145 Some(ctx.caster.id),
146 ctx.caster,
147 total as f64 * mods.leech_fraction,
148 derived_source,
149 )
150 .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
151 }
152
153 Ok(primary_damage)
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use essences::entity::Coordinates;
160 use essences::fighting::EntityTeam;
161
162 fn enemy(id: u128) -> Entity {
163 Entity {
164 id: Uuid::from_u128(id),
165 team: EntityTeam::Enemy,
166 hp: 100,
167 max_hp: 100,
168 coordinates: Coordinates { x: 1, y: 0 },
169 ..Default::default()
170 }
171 }
172
173 #[test]
174 fn extra_targets_skips_the_primary_and_the_dead() {
175 let caster = Entity {
176 id: Uuid::from_u128(99),
177 team: EntityTeam::Ally,
178 ..Default::default()
179 };
180 let mut dead = enemy(3);
181 dead.hp = 0;
182 let fight = ActiveFight {
183 entities: vec![enemy(1), dead, enemy(2), enemy(4)],
184 ..Default::default()
185 };
186 let primary = enemy(1);
187
188 let ids: Vec<Uuid> = extra_targets(&fight, &caster, &primary, 2)
189 .iter()
190 .map(|e| e.id)
191 .collect();
192
193 assert_eq!(ids, vec![Uuid::from_u128(2), Uuid::from_u128(4)]);
194 }
195
196 #[test]
197 fn a_derived_copy_is_tagged_and_cannot_counter() {
198 let base = AttackParams {
199 power: Some(2.0),
200 dot_power: Some(1.0),
201 ..Default::default()
202 };
203 let copy = derived_params(&base, 0.7);
204
205 assert_eq!(copy.power, Some(1.4));
206 assert_eq!(copy.dot_power, Some(0.7));
207 assert!(copy.derived);
208 assert!(copy.no_counterattack);
209 }
210}