1use configs::game_config::GameConfig;
28use essences::combat_origin::CombatEventOrigin;
29use essences::entity::Entity;
30use essences::fight_breakdown::CombatSource;
31use essences::fighting::ActiveFight;
32use event_system::script::random::GameRng;
33use uuid::Uuid;
34
35use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
36use crate::event::OverlordEvent;
37use crate::game_config_helpers::GameConfigLookup;
38use crate::mechanics::content::ability_info;
39use crate::mechanics::content_lookups::ContentLookups;
40use crate::mechanics::effect_cb::OverlordEffectCb;
41use crate::mechanics::fight::{
42 self, AttackParams, NativeSink, SpellHealParams, on_cast, spell_heal, stat_throw,
43};
44use crate::mechanics::support_shapes::{SupportAttackCtx, attack_with_supports};
45use essences::ability_stones::AbilityStoneMods;
46
47pub struct CastAbilityCtx<'a> {
49 pub caster_entity: &'a Entity,
51 pub target_entity: &'a Entity,
53 pub fight: &'a ActiveFight,
55 pub rng: &'a GameRng,
57 pub ability_level: i64,
59 pub ability_id: uuid::Uuid,
63 pub config: &'a GameConfig,
64 pub lookups: &'a ContentLookups,
65 pub stones: crate::mechanics::ability_stones::AbilityStoneResolver<'a>,
69 pub source: CombatSource,
73}
74
75pub type CastAbilityFn = fn(&CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>>;
77
78fn run_on_cast(ctx: &CastAbilityCtx, sink: &mut NativeSink) -> anyhow::Result<()> {
81 let mut effects = OverlordEffectCb;
82 on_cast(sink, ctx.rng, ctx.lookups, &mut effects, ctx.caster_entity)
83 .map_err(|e| anyhow::anyhow!("on_cast: {e}"))
84}
85
86fn run_attack(
90 ctx: &CastAbilityCtx,
91 sink: &mut NativeSink,
92 mods: &AbilityStoneMods,
93 target: &Entity,
94 params: &AttackParams,
95) -> anyhow::Result<Option<i64>> {
96 attack_with_supports(
97 sink,
98 &SupportAttackCtx {
99 rng: ctx.rng,
100 lookups: ctx.lookups,
101 fight: ctx.fight,
102 player_id: ctx.fight.player_id,
103 caster: ctx.caster_entity,
104 mods,
105 source: ctx.source,
106 },
107 target,
108 params,
109 )
110}
111
112fn start_cast_projectile(ctx: &CastAbilityCtx, projectile_id: &str, delay: u64) -> OverlordEvent {
114 OverlordEvent::StartCastProjectile {
115 by_entity_id: ctx.caster_entity.id,
116 to_entity_id: ctx.target_entity.id,
117 projectile_id: Uuid::parse_str(projectile_id).expect("valid projectile uuid literal"),
118 level: ctx.ability_level,
119 delay,
120 origin: CombatEventOrigin::Core,
121 source: ctx.source,
122 }
123}
124
125fn info(
129 ctx: &CastAbilityCtx,
130 ability_id: &str,
131) -> anyhow::Result<(crate::mechanics::content::AbilityInfo, AbilityStoneMods)> {
132 let id = Uuid::parse_str(ability_id).expect("valid ability uuid literal");
133 let mut info = ability_info(ctx.config, ctx.lookups, id, ctx.ability_level)?;
134 let mods = ctx.stones.mods_for(ctx.config, id, ctx.ability_level);
135 info.apply_stone_mods(&mods);
136 Ok((info, mods))
137}
138
139pub fn band_targets<'a>(
145 fight: &'a ActiveFight,
146 caster: &Entity,
147 max_targets: Option<i64>,
148) -> Vec<&'a Entity> {
149 let caster_x = caster.coordinates.x;
150 let mut out: Vec<&Entity> = Vec::new();
151 for target in fight.entities.iter().filter(|e| e.team != caster.team) {
152 let tx = target.coordinates.x;
153 if tx > caster_x && tx <= caster_x + 3 {
154 if max_targets.is_some_and(|cap| out.len() as i64 >= cap) {
155 break;
156 }
157 out.push(target);
158 }
159 }
160 out
161}
162
163fn on_cast_then_projectile(
170 ctx: &CastAbilityCtx,
171 projectile_id: &str,
172) -> anyhow::Result<Vec<OverlordEvent>> {
173 let mut sink = NativeSink::default();
174 run_on_cast(ctx, &mut sink)?;
175 sink.events
176 .push(start_cast_projectile(ctx, projectile_id, 0));
177 Ok(sink.events)
178}
179
180fn bare_projectile(
183 ctx: &CastAbilityCtx,
184 projectile_id: &str,
185) -> anyhow::Result<Vec<OverlordEvent>> {
186 Ok(vec![start_cast_projectile(ctx, projectile_id, 0)])
187}
188
189fn on_cast_then_attack(
191 ctx: &CastAbilityCtx,
192 ability_id: &str,
193) -> anyhow::Result<Vec<OverlordEvent>> {
194 let mut sink = NativeSink::default();
195 run_on_cast(ctx, &mut sink)?;
196 let (ability_info, mods) = info(ctx, ability_id)?;
197 let params = AttackParams {
198 power: ability_info.damage,
199 ..Default::default()
200 };
201 run_attack(ctx, &mut sink, &mods, ctx.target_entity, ¶ms)?;
202 Ok(sink.events)
203}
204
205pub fn melee_strike(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
211 on_cast_then_attack(ctx, "0194d64e-20f2-75e5-89c8-4cb812672485")
212}
213
214pub fn sword_slash(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
216 on_cast_then_attack(ctx, "019bff40-af44-75b7-940c-6074097a2925")
217}
218
219pub fn mob_melee_quick(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
221 on_cast_then_attack(ctx, "019cc464-e752-71c1-a9dd-8fda9f212801")
222}
223
224pub fn mob_melee_average(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
226 on_cast_then_attack(ctx, "019cc465-14b8-7dbc-9799-4691b91805d3")
227}
228
229pub fn mob_melee_slow(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
231 on_cast_then_attack(ctx, "019cc465-2f63-7b54-8ddc-fcbcb483fe81")
232}
233
234pub fn apply_test_effect(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
237 let mut sink = NativeSink::default();
238 let mut effects = OverlordEffectCb;
239 fight::apply_entity_effect(
240 &mut sink,
241 ctx.lookups,
242 &mut effects,
243 ctx.caster_entity,
244 "test_effect",
245 3.0,
246 )
247 .map_err(|e| anyhow::anyhow!("apply_entity_effect: {e}"))?;
248 Ok(sink.events)
249}
250
251pub fn crit_strike(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
253 let mut sink = NativeSink::default();
254 run_on_cast(ctx, &mut sink)?;
255 let (ability_info, mods) = info(ctx, "019584aa-5bde-7ac2-8850-076dafdc4603")?;
256 let params = AttackParams {
257 power: ability_info.damage,
258 crit_chance_bonus: ability_info.crit_chance_bonus.unwrap_or(0.0) * 10000.0,
259 ..Default::default()
260 };
261 run_attack(ctx, &mut sink, &mods, ctx.target_entity, ¶ms)?;
262 Ok(sink.events)
263}
264
265pub fn cone_strike(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
268 let mut sink = NativeSink::default();
269 run_on_cast(ctx, &mut sink)?;
270 let (ability_info, mods) = info(ctx, "019584f4-2c99-72bf-bcd3-bfc02fb33977")?;
271 let is_crit = stat_throw(ctx.rng, ctx.lookups, ctx.caster_entity, "crit_chance", 0.0);
273 for target in band_targets(ctx.fight, ctx.caster_entity, ability_info.max_targets) {
276 let params = AttackParams {
277 power: ability_info.damage,
278 is_crit: Some(is_crit),
279 ..Default::default()
280 };
281 run_attack(ctx, &mut sink, &mods, target, ¶ms)?;
282 }
283 Ok(sink.events)
284}
285
286pub fn multi_projectile(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
289 const PROJECTILE_DELAY: i64 = 150;
290 let mut sink = NativeSink::default();
291 run_on_cast(ctx, &mut sink)?;
292 let (ability_info, _mods) = info(ctx, "019589e6-f9dd-7b22-8d39-5350e95aaf69")?;
293 let projectiles = ability_info.projectiles.unwrap_or(0);
294 for i in 0..projectiles {
295 let delay = (i * PROJECTILE_DELAY) as u64;
296 sink.events.push(start_cast_projectile(
297 ctx,
298 "0196a6b3-f885-7fdc-af8c-92a1ffb79ceb",
299 delay,
300 ));
301 }
302 Ok(sink.events)
303}
304
305pub fn strike_then_empower(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
308 let mut sink = NativeSink::default();
309 run_on_cast(ctx, &mut sink)?;
310 let (ability_info, mods) = info(ctx, "01958a30-8f18-745f-928a-75028cb3ee99")?;
311 let params = AttackParams {
312 power: ability_info.damage,
313 ..Default::default()
314 };
315 let damage = run_attack(ctx, &mut sink, &mods, ctx.target_entity, ¶ms)?;
316 if damage.is_some() {
317 let mut effects = OverlordEffectCb;
318 fight::apply_entity_effect(
319 &mut sink,
320 ctx.lookups,
321 &mut effects,
322 ctx.caster_entity,
323 "empower",
324 ability_info.effect_duration.unwrap_or(0.0),
325 )
326 .map_err(|e| anyhow::anyhow!("apply_entity_effect: {e}"))?;
327 }
328 Ok(sink.events)
329}
330
331pub fn strike_then_vulnerability(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
334 let mut sink = NativeSink::default();
335 run_on_cast(ctx, &mut sink)?;
336 let (ability_info, mods) = info(ctx, "01958ef2-dff5-76dd-89f1-d9c2707b2ffc")?;
337 let params = AttackParams {
338 power: ability_info.damage,
339 ..Default::default()
340 };
341 let damage = run_attack(ctx, &mut sink, &mods, ctx.target_entity, ¶ms)?;
342 if damage.is_some() {
343 let mut effects = OverlordEffectCb;
344 fight::apply_entity_effect(
345 &mut sink,
346 ctx.lookups,
347 &mut effects,
348 ctx.target_entity,
349 "vulnerability",
350 ability_info.effect_duration.unwrap_or(0.0),
351 )
352 .map_err(|e| anyhow::anyhow!("apply_entity_effect: {e}"))?;
353 }
354 Ok(sink.events)
355}
356
357pub fn night_pounce_projectile_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
362 on_cast_then_projectile(ctx, "79f90288-9ff4-46ee-9b11-8a0064913415")
363}
364
365pub fn rust_wave_projectile_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
366 on_cast_then_projectile(ctx, "1ce4597d-dd7a-4c5c-834b-134ef532c0c2")
367}
368
369pub fn odd_flame_projectile_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
370 on_cast_then_projectile(ctx, "432f231c-75eb-4753-acca-b6356ca8104c")
371}
372
373pub fn dot_strike(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
376 let mut sink = NativeSink::default();
377 run_on_cast(ctx, &mut sink)?;
378 let (ability_info, mods) = info(ctx, "01958efd-77f9-7dec-8444-c9d759549225")?;
379 let params = AttackParams {
380 dot_power: ability_info.damage,
381 ..Default::default()
382 };
383 run_attack(ctx, &mut sink, &mods, ctx.target_entity, ¶ms)?;
384 Ok(sink.events)
385}
386
387pub fn self_hot_heal(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
389 let mut sink = NativeSink::default();
390 run_on_cast(ctx, &mut sink)?;
391 let (ability_info, _mods) = info(ctx, "01958ed0-d45f-7cad-b086-8f11962d3859")?;
392 let params = SpellHealParams {
393 hot_power: ability_info.hot,
394 ..Default::default()
395 };
396 spell_heal(
397 &mut sink,
398 ctx.rng,
399 ctx.lookups,
400 ctx.caster_entity,
401 ctx.caster_entity,
402 ¶ms,
403 ctx.source,
404 )
405 .map_err(|e| anyhow::anyhow!("spell_heal: {e}"))?;
406 Ok(sink.events)
407}
408
409fn heal_max_hp_share(
423 sink: &mut NativeSink,
424 by_entity_id: Option<essences::entity::EntityId>,
425 recipient: &essences::entity::Entity,
426 share: f64,
427 source: CombatSource,
428) -> anyhow::Result<()> {
429 if share <= 0.0 || recipient.max_hp == 0 {
430 return Ok(());
431 }
432 fight::heal_entity(
433 sink,
434 by_entity_id,
435 recipient,
436 recipient.max_hp as f64 * share,
437 source,
438 )
439 .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
440 Ok(())
441}
442
443fn lowest_hp_share_ally<'a>(
447 fight: &'a essences::fighting::ActiveFight,
448 caster: &'a essences::entity::Entity,
449) -> &'a essences::entity::Entity {
450 fight
451 .entities
452 .iter()
453 .filter(|entity| entity.team == caster.team && entity.hp > 0 && entity.max_hp > 0)
454 .min_by(|a, b| {
455 let share = |e: &essences::entity::Entity| e.hp as f64 / e.max_hp as f64;
456 share(a)
457 .partial_cmp(&share(b))
458 .unwrap_or(std::cmp::Ordering::Equal)
459 })
460 .unwrap_or(caster)
461}
462
463pub fn backstab_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
466 const EXECUTE_HP_SHARE: f64 = 0.30;
468 const EXECUTE_MULT: f64 = 1.70;
470
471 let mut sink = NativeSink::default();
472 run_on_cast(ctx, &mut sink)?;
473 let (ability_info, mods) = info(ctx, "019dfc4c-75ea-716c-a453-801c968be604")?;
474
475 let target = ctx.target_entity;
478 let wounded = target.max_hp > 0 && (target.hp as f64 / target.max_hp as f64) < EXECUTE_HP_SHARE;
479 let power = ability_info.damage.map(|damage| {
480 if wounded {
481 damage * EXECUTE_MULT
482 } else {
483 damage
484 }
485 });
486
487 let params = AttackParams {
488 power,
489 is_crit: Some(true),
490 ..Default::default()
491 };
492 run_attack(ctx, &mut sink, &mods, target, ¶ms)?;
493 Ok(sink.events)
494}
495
496pub fn thousand_cuts_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
500 let mut sink = NativeSink::default();
501 run_on_cast(ctx, &mut sink)?;
502 let (ability_info, mods) = info(ctx, "019dfc4f-5c4f-7829-affa-11b21a735f78")?;
503 let cuts = ability_info.projectiles.unwrap_or(1).max(1);
504 for i in 0..cuts {
505 let params = AttackParams {
506 power: ability_info.damage,
507 no_counterattack: i > 0,
510 ..Default::default()
511 };
512 run_attack(ctx, &mut sink, &mods, ctx.target_entity, ¶ms)?;
513 }
514 Ok(sink.events)
515}
516
517pub fn arcane_blast_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
522 const SECONDARY_SHARE: f64 = 0.4;
524
525 let mut sink = NativeSink::default();
526 run_on_cast(ctx, &mut sink)?;
527 let (ability_info, mods) = info(ctx, "019dfcfb-7b13-7cf2-b8e3-ab9546310c2b")?;
528
529 let primary = AttackParams {
530 power: ability_info.damage,
531 ..Default::default()
532 };
533 run_attack(ctx, &mut sink, &mods, ctx.target_entity, &primary)?;
534
535 let secondary_power = ability_info.damage.map(|damage| damage * SECONDARY_SHARE);
538 for target in band_targets(ctx.fight, ctx.caster_entity, None) {
539 if target.id == ctx.target_entity.id {
540 continue;
541 }
542 let params = AttackParams {
543 power: secondary_power,
544 ..Default::default()
545 };
546 run_attack(ctx, &mut sink, &mods, target, ¶ms)?;
547 }
548 Ok(sink.events)
549}
550
551const REWIND_COOLDOWN_CUT_TICKS: i64 = 2_000;
555
556const REWIND_ABILITY_ID: uuid::Uuid = uuid::uuid!("019dfcfe-704c-73cf-b6e8-60f85e86799d");
558
559pub fn rewind_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
560 let mut sink = NativeSink::default();
561 run_on_cast(ctx, &mut sink)?;
562 let (ability_info, mods) = info(ctx, "019dfcfe-704c-73cf-b6e8-60f85e86799d")?;
563 let is_crit = stat_throw(ctx.rng, ctx.lookups, ctx.caster_entity, "crit_chance", 0.0);
564 let duration = ability_info.effect_duration.unwrap_or(0.0);
565 for target in band_targets(ctx.fight, ctx.caster_entity, ability_info.max_targets) {
566 let params = AttackParams {
567 power: ability_info.damage,
568 is_crit: Some(is_crit),
569 ..Default::default()
570 };
571 let dealt = run_attack(ctx, &mut sink, &mods, target, ¶ms)?;
572 if dealt.is_some() && duration > 0.0 {
573 let mut effects = OverlordEffectCb;
574 fight::apply_entity_effect(
575 &mut sink,
576 ctx.lookups,
577 &mut effects,
578 target,
579 "weakness",
580 duration,
581 )
582 .map_err(|e| anyhow::anyhow!("apply_entity_effect: {e}"))?;
583 }
584 }
585
586 for ability in &ctx.caster_entity.abilities {
590 if ability.ability.template_id == REWIND_ABILITY_ID {
591 continue;
592 }
593 sink.events.push(OverlordEvent::EntityAddAbilityCooldown {
594 entity_id: ctx.caster_entity.id,
595 ability_id: ability.ability.template_id,
596 delta_ticks: -REWIND_COOLDOWN_CUT_TICKS,
597 });
598 }
599 Ok(sink.events)
600}
601
602pub fn fortify_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
605 let mut sink = NativeSink::default();
606 run_on_cast(ctx, &mut sink)?;
607 let (ability_info, _mods) = info(ctx, "019dfd00-594e-755f-8132-1c320fb2b5e9")?;
608 if let Some(duration) = ability_info.effect_duration.filter(|d| *d > 0.0) {
609 let mut effects = OverlordEffectCb;
610 fight::apply_entity_effect(
611 &mut sink,
612 ctx.lookups,
613 &mut effects,
614 ctx.caster_entity,
615 "protection",
616 duration,
617 )
618 .map_err(|e| anyhow::anyhow!("apply_entity_effect: {e}"))?;
619 }
620 heal_max_hp_share(
622 &mut sink,
623 Some(ctx.caster_entity.id),
624 ctx.caster_entity,
625 ability_info.hot.unwrap_or(0.0),
626 ctx.source,
627 )?;
628 Ok(sink.events)
629}
630
631pub fn war_cry_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
634 let mut sink = NativeSink::default();
635 run_on_cast(ctx, &mut sink)?;
636 let (ability_info, mods) = info(ctx, "019dfd01-073e-7aee-a993-74e20b3c439c")?;
637 let is_crit = stat_throw(ctx.rng, ctx.lookups, ctx.caster_entity, "crit_chance", 0.0);
638 let mut any_hit = false;
639 for target in band_targets(ctx.fight, ctx.caster_entity, ability_info.max_targets) {
640 let params = AttackParams {
641 power: ability_info.damage,
642 is_crit: Some(is_crit),
643 ..Default::default()
644 };
645 any_hit |= run_attack(ctx, &mut sink, &mods, target, ¶ms)?.is_some();
646 }
647 if any_hit && let Some(duration) = ability_info.effect_duration.filter(|d| *d > 0.0) {
648 let mut effects = OverlordEffectCb;
649 fight::apply_entity_effect(
650 &mut sink,
651 ctx.lookups,
652 &mut effects,
653 ctx.caster_entity,
654 "war_fury",
655 duration,
656 )
657 .map_err(|e| anyhow::anyhow!("apply_entity_effect: {e}"))?;
658 }
659 Ok(sink.events)
660}
661
662pub fn battle_heal_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
667 let mut sink = NativeSink::default();
668 run_on_cast(ctx, &mut sink)?;
669 let (ability_info, _mods) = info(ctx, "019dfd02-0c6a-7f4d-bb45-4ec2a5fc231a")?;
670
671 let allies: Vec<_> = ctx
672 .fight
673 .entities
674 .iter()
675 .filter(|entity| entity.team == ctx.caster_entity.team && entity.hp > 0)
676 .cloned()
677 .collect();
678
679 for ally in &allies {
680 heal_max_hp_share(
682 &mut sink,
683 Some(ctx.caster_entity.id),
684 ally,
685 ability_info.hot.unwrap_or(0.0),
686 ctx.source,
687 )?;
688
689 if let Some(duration) = ability_info.effect_duration.filter(|d| *d > 0.0) {
693 let mut effects = OverlordEffectCb;
694 fight::apply_entity_effect(
695 &mut sink,
696 ctx.lookups,
697 &mut effects,
698 ally,
699 "battle_blessing",
700 duration,
701 )
702 .map_err(|e| anyhow::anyhow!("apply_entity_effect: {e}"))?;
703 }
704 }
705 Ok(sink.events)
706}
707
708pub fn holy_nova_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
712 let mut sink = NativeSink::default();
713 run_on_cast(ctx, &mut sink)?;
714 let (ability_info, mods) = info(ctx, "019dfd02-add4-7373-912c-8483150fd341")?;
715 let is_crit = stat_throw(ctx.rng, ctx.lookups, ctx.caster_entity, "crit_chance", 0.0);
716 for target in band_targets(ctx.fight, ctx.caster_entity, ability_info.max_targets) {
717 let params = AttackParams {
718 power: ability_info.damage,
719 is_crit: Some(is_crit),
720 ..Default::default()
721 };
722 run_attack(ctx, &mut sink, &mods, target, ¶ms)?;
723 }
724 let recipient = lowest_hp_share_ally(ctx.fight, ctx.caster_entity).clone();
728 heal_max_hp_share(
729 &mut sink,
730 Some(ctx.caster_entity.id),
731 &recipient,
732 ability_info.hot.unwrap_or(0.0),
733 ctx.source,
734 )?;
735 Ok(sink.events)
736}
737
738pub fn fireball_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
742 on_cast_then_projectile(ctx, "01966cbc-879d-7b00-b1de-ad8ed932fb63")
743}
744
745pub fn fireball_projectile_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
750 on_cast_then_projectile(ctx, "01966cbc-879d-7b00-b1de-ad8ed932fb63")
751}
752
753pub fn vampiric_touch_cast(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
755 on_cast_then_projectile(ctx, "0196a6b4-cb8c-7bcb-a99c-1f0189dd8d5f")
756}
757
758pub fn bare_projectile_mushroom(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
762 bare_projectile(ctx, "019a0244-4675-7e4b-868a-c9b8ef46a091")
763}
764
765pub fn bare_projectile_chipmunk(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
767 bare_projectile(ctx, "019a0244-7a0a-7241-bdae-4541d9f972f6")
768}
769
770pub fn bare_projectile_bat(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
772 bare_projectile(ctx, "019a0244-aad4-7ae3-ae11-25d5facadf17")
773}
774
775pub fn bare_projectile_shot(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
777 bare_projectile(ctx, "019c009c-22b2-7fa7-9599-97eb239b13b9")
778}
779
780pub fn damage_target_5(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
788 Ok(vec![OverlordEvent::Damage {
789 by_entity_id: Some(ctx.caster_entity.id),
790 entity_id: ctx.target_entity.id,
791 damage: 5,
792 damage_data: crate::event::CustomEventData::default(),
793 origin: CombatEventOrigin::Core,
794 source: ctx.source,
795 }])
796}
797
798pub fn test_swing(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
810 let mut sink = NativeSink::default();
811 let params = AttackParams {
812 power: Some(configs::tests_game_config::TEST_SWING_POWER),
813 is_crit: Some(false),
814 no_counterattack: true,
815 ..Default::default()
816 };
817 let mods = ctx
818 .stones
819 .mods_for(ctx.config, ctx.ability_id, ctx.ability_level);
820 run_attack(ctx, &mut sink, &mods, ctx.target_entity, ¶ms)?;
821 Ok(sink.events)
822}
823
824pub fn test_swing_aoe(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
832 let mut sink = NativeSink::default();
833 let params = AttackParams {
834 power: Some(configs::tests_game_config::TEST_SWING_POWER),
835 is_crit: Some(false),
836 no_counterattack: true,
837 ..Default::default()
838 };
839 let targets: Vec<&Entity> = ctx
840 .fight
841 .entities
842 .iter()
843 .filter(|entity| entity.team != ctx.caster_entity.team && entity.hp > 0)
844 .collect();
845 let mods = ctx
846 .stones
847 .mods_for(ctx.config, ctx.ability_id, ctx.ability_level);
848 for target in targets {
849 run_attack(ctx, &mut sink, &mods, target, ¶ms)?;
850 }
851 Ok(sink.events)
852}
853
854pub fn apply_bloodleak_effect(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
857 Ok(vec![
858 OverlordEvent::EntityIncrAttribute {
859 entity_id: ctx.target_entity.id,
860 attribute: "bloodleak".to_string(),
861 delta: 10,
862 },
863 OverlordEvent::EntityApplyEffect {
864 entity_id: ctx.target_entity.id,
865 effect_id: Uuid::parse_str("ccc47912-61c0-4efa-88f9-7911fa1b074f")?,
866 origin: CombatEventOrigin::Core,
867 },
868 ])
869}
870
871pub fn test_band_strike(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
876 const FALLBACK_POWER: f64 = 5.0;
878 let mut sink = NativeSink::default();
879 let mods = ctx
880 .stones
881 .mods_for(ctx.config, ctx.ability_id, ctx.ability_level);
882 let mut info = ability_info(ctx.config, ctx.lookups, ctx.ability_id, ctx.ability_level)
885 .unwrap_or_else(|_| crate::mechanics::content::AbilityInfo {
886 damage: Some(FALLBACK_POWER),
887 ..crate::mechanics::content::AbilityInfo::none()
888 });
889
890 if info.damage.is_none() {
891 info.damage = Some(FALLBACK_POWER);
892 }
893 info.max_targets = ctx
894 .config
895 .ability_template(ctx.ability_id)
896 .and_then(|template| template.max_targets);
897 info.apply_stone_mods(&mods);
898 for target in band_targets(ctx.fight, ctx.caster_entity, info.max_targets) {
899 let params = AttackParams {
900 power: info.damage,
901 ..Default::default()
902 };
903 run_attack(ctx, &mut sink, &mods, target, ¶ms)?;
904 }
905 Ok(sink.events)
906}
907
908pub fn apply_low_hp_heal_effect(ctx: &CastAbilityCtx) -> anyhow::Result<Vec<OverlordEvent>> {
913 Ok(vec![OverlordEvent::EntityApplyEffect {
914 entity_id: ctx.caster_entity.id,
915 effect_id: Uuid::parse_str("3b136901-137c-47cc-8c7e-bc0ce387eb1c")?,
916 origin: CombatEventOrigin::Core,
917 }])
918}
919
920pub fn register(registry: &mut BehaviorRegistry) {
922 let mut reg = |name: &str, title: &str, desc: &str, f: CastAbilityFn| {
923 registry.register_cast_ability(
924 BehaviorMeta {
925 name: name.to_string(),
926 category: BehaviorKind::CastAbility,
927 title: title.to_string(),
928 description: desc.to_string(),
929 },
930 f,
931 );
932 };
933
934 reg(
935 "ability_melee_strike",
936 "Способность: удар по цели (0194d64e)",
937 "on_cast + attack{power} (порт ability script 0194d64e).",
938 melee_strike,
939 );
940 reg(
941 "ability_sword_slash",
942 "Способность: удар по цели (019bff40)",
943 "on_cast + attack{power} (порт ability script 019bff40).",
944 sword_slash,
945 );
946 reg(
947 "ability_mob_melee_quick",
948 "Способность: удар по цели (019cc464)",
949 "on_cast + attack{power} (порт ability script 019cc464).",
950 mob_melee_quick,
951 );
952 reg(
953 "ability_mob_melee_average",
954 "Способность: удар по цели (019cc465-14b8)",
955 "on_cast + attack{power} (порт ability script 019cc465-14b8).",
956 mob_melee_average,
957 );
958 reg(
959 "ability_mob_melee_slow",
960 "Способность: удар по цели (019cc465-2f63)",
961 "on_cast + attack{power} (порт ability script 019cc465-2f63).",
962 mob_melee_slow,
963 );
964 reg(
965 "ability_apply_test_effect",
966 "Способность: наложить test_effect",
967 "apply_entity_effect(Caster, test_effect, 3) (порт ability script 01955be6).",
968 apply_test_effect,
969 );
970 reg(
971 "ability_damage_target_5",
972 "Способность: урон 5 по цели (тест)",
973 "Damage(TargetEntity, 5) — порт общего test ability script.",
974 damage_target_5,
975 );
976 reg(
977 "ability_apply_bloodleak_effect",
978 "Способность: bloodleak +10 + эффект (тест 50260b1c)",
979 "IncrAttribute(Target, bloodleak, 10) + ApplyEffect(Target, ccc47912).",
980 apply_bloodleak_effect,
981 );
982 reg(
983 "ability_test_band_strike",
984 "Способность: тестовый удар по зоне (фикстура)",
985 "attack по врагам в 3-клеточной зоне; охват и payload берутся из шаблона \
986 кастуемой способности и её камней поддержки (только для тестового конфига).",
987 test_band_strike,
988 );
989 reg(
990 "ability_test_swing_aoe",
991 "Способность: настоящий замах по всем врагам (тест)",
992 "attack по каждому живому врагу в тике самого каста — для условий на число целей.",
993 test_swing_aoe,
994 );
995 reg(
996 "ability_test_swing",
997 "Способность: настоящий замах (тест)",
998 "attack{power: TEST_SWING_POWER, is_crit: false} — единственная тестовая \
999 способность, которая доходит до примитивов урона.",
1000 test_swing,
1001 );
1002 reg(
1003 "ability_apply_low_hp_heal_effect",
1004 "Способность: наложить heal-эффект на кастера (тест 41ee5532)",
1005 "ApplyEffect(Caster, 3b136901).",
1006 apply_low_hp_heal_effect,
1007 );
1008 reg(
1009 "ability_crit_strike",
1010 "Способность: удар с бонусом крита (019584aa)",
1011 "on_cast + attack{power, crit_chance_bonus} (порт ability script 019584aa).",
1012 crit_strike,
1013 );
1014 reg(
1015 "ability_cone_strike",
1016 "Способность: конус по фронтальным врагам (019584f4)",
1017 "on_cast, один общий crit-бросок, attack по врагам в 3-клеточной зоне по x \
1018 (порт ability script 019584f4).",
1019 cone_strike,
1020 );
1021 reg(
1022 "ability_multi_projectile",
1023 "Способность: серия снарядов (019589e6)",
1024 "on_cast + N снарядов 0196a6b3 с задержкой i*150 (порт ability script 019589e6).",
1025 multi_projectile,
1026 );
1027 reg(
1028 "ability_strike_then_empower",
1029 "Способность: удар + empower (01958a30)",
1030 "on_cast + attack{power}; при уроне накладывает empower (порт ability script 01958a30).",
1031 strike_then_empower,
1032 );
1033 reg(
1034 "ability_strike_then_vulnerability",
1035 "Способность: удар + vulnerability (01958ef2)",
1036 "on_cast + attack{power}; при уроне накладывает vulnerability на цель \
1037 (порт ability script 01958ef2).",
1038 strike_then_vulnerability,
1039 );
1040 reg(
1041 "ability_night_pounce_projectile",
1042 "Способность: ульта питомца Nyx снарядом (79f90288)",
1043 "on_cast + StartCastProjectile(79f90288); урон ability_crit_strike \
1044 переносится на прилёт снаряда.",
1045 night_pounce_projectile_cast,
1046 );
1047 reg(
1048 "ability_rust_wave_projectile",
1049 "Способность: ульта питомца Rusty снарядом (1ce4597d)",
1050 "on_cast + StartCastProjectile(1ce4597d); конус ability_cone_strike \
1051 переносится на прилёт снаряда.",
1052 rust_wave_projectile_cast,
1053 );
1054 reg(
1055 "ability_odd_flame_projectile",
1056 "Способность: ульта питомца Thingy снарядом (432f231c)",
1057 "on_cast + StartCastProjectile(432f231c); DoT ability_dot_strike \
1058 переносится на прилёт снаряда.",
1059 odd_flame_projectile_cast,
1060 );
1061 reg(
1062 "ability_dot_strike",
1063 "Способность: чистый DoT-удар (01958efd)",
1064 "on_cast + attack{dot_power} (порт ability script 01958efd).",
1065 dot_strike,
1066 );
1067 reg(
1068 "ability_self_hot_heal",
1069 "Способность: HoT-лечение себя (01958ed0)",
1070 "on_cast + spell_heal(Caster, Caster, {hot_power}) (порт ability script 01958ed0).",
1071 self_hot_heal,
1072 );
1073 reg(
1074 "ability_fireball",
1075 "Способность: запуск снаряда 01966cbc (01958172)",
1076 "on_cast + StartCastProjectile(01966cbc) (порт ability script 01958172).",
1077 fireball_cast,
1078 );
1079 reg(
1080 "ability_fireball_projectile_shared",
1081 "Способность: запуск снаряда 01966cbc (общий)",
1082 "on_cast + StartCastProjectile(01966cbc); общий для 8 идентичных ability script \
1083 (019dfc4c/019dfc4f/019dfcfb/019dfcfe/019dfd00/019dfd01/019dfd02-0c6a/019dfd02-add4).",
1084 fireball_projectile_cast,
1085 );
1086 reg(
1087 "ability_vampiric_touch",
1088 "Способность: запуск снаряда 0196a6b4 (019589f7)",
1089 "on_cast + StartCastProjectile(0196a6b4) (порт ability script 019589f7).",
1090 vampiric_touch_cast,
1091 );
1092 reg(
1093 "ability_bare_projectile_mushroom",
1094 "Способность: голый запуск снаряда 019a0244-4675 (019a0245)",
1095 "StartCastProjectile(019a0244-4675) без on_cast (порт ability script 019a0245-ff0c).",
1096 bare_projectile_mushroom,
1097 );
1098 reg(
1099 "ability_bare_projectile_chipmunk",
1100 "Способность: голый запуск снаряда 019a0244-7a0a (019a0246-5aaf)",
1101 "StartCastProjectile(019a0244-7a0a) без on_cast (порт ability script 019a0246-5aaf).",
1102 bare_projectile_chipmunk,
1103 );
1104 reg(
1105 "ability_bare_projectile_bat",
1106 "Способность: голый запуск снаряда 019a0244-aad4 (019a0246-cf87)",
1107 "StartCastProjectile(019a0244-aad4) без on_cast (порт ability script 019a0246-cf87).",
1108 bare_projectile_bat,
1109 );
1110 reg(
1111 "ability_bare_projectile_shot",
1112 "Способность: голый запуск снаряда 019c009c (019c00a4)",
1113 "StartCastProjectile(019c009c) без on_cast (порт ability script 019c00a4).",
1114 bare_projectile_shot,
1115 );
1116
1117 reg(
1119 "ability_backstab",
1120 "Класс-кит Rogue: Backstab (019dfc4c)",
1121 "on_cast + attack{power, +50% crit} — крит-гэмбл кита.",
1122 backstab_cast,
1123 );
1124 reg(
1125 "ability_thousand_cuts",
1126 "Класс-кит Rogue: Thousand Cuts (019dfc4f)",
1127 "on_cast + 5 прямых ударов по цели (контратака возможна только с первого).",
1128 thousand_cuts_cast,
1129 );
1130 reg(
1131 "ability_arcane_blast",
1132 "Класс-кит Mage: Arcane Blast (019dfcfb)",
1133 "on_cast + attack{power} — весь бюджет одним нюком.",
1134 arcane_blast_cast,
1135 );
1136 reg(
1137 "ability_rewind",
1138 "Класс-кит Mage: Rewind (019dfcfe)",
1139 "on_cast + конус-удар + Weakness на поражённых врагов.",
1140 rewind_cast,
1141 );
1142 reg(
1143 "ability_fortify",
1144 "Класс-кит Warrior: Fortify (019dfd00)",
1145 "on_cast + Protection на себя + инстант-хил остатком бюджета.",
1146 fortify_cast,
1147 );
1148 reg(
1149 "ability_war_cry",
1150 "Класс-кит Warrior: War Cry (019dfd01)",
1151 "on_cast + конус-удар + Empower на себя при попадании.",
1152 war_cry_cast,
1153 );
1154 reg(
1155 "ability_battle_heal",
1156 "Класс-кит Priest: Battle heal (019dfd02-0c6a)",
1157 "on_cast + spell_heal(Caster, Caster, {power}) — инстант-хил всем бюджетом.",
1158 battle_heal_cast,
1159 );
1160 reg(
1161 "ability_holy_nova",
1162 "Класс-кит Priest: Holy Nova (019dfd02-add4)",
1163 "on_cast + конус-удар; кастер лечится за долю нанесённого урона.",
1164 holy_nova_cast,
1165 );
1166}
1167
1168#[cfg(test)]
1169mod band_targets_tests {
1170 use super::band_targets;
1171 use essences::entity::{Coordinates, Entity};
1172 use essences::fighting::{ActiveFight, EntityTeam};
1173
1174 fn enemy_at(id: u128, x: i64) -> Entity {
1175 Entity {
1176 id: uuid::Uuid::from_u128(id),
1177 team: EntityTeam::Enemy,
1178 coordinates: Coordinates { x, y: 0 },
1179 ..Default::default()
1180 }
1181 }
1182
1183 #[test]
1187 fn caps_first_n_in_fight_order() {
1188 let caster = Entity {
1189 team: EntityTeam::Ally,
1190 coordinates: Coordinates { x: 0, y: 0 },
1191 ..Default::default()
1192 };
1193 let fight = ActiveFight {
1194 entities: vec![
1195 enemy_at(1, 1),
1196 enemy_at(2, 2),
1197 enemy_at(3, 3),
1198 enemy_at(4, 3),
1199 enemy_at(5, 2),
1200 ],
1201 ..Default::default()
1202 };
1203 let ids = |cap| -> Vec<uuid::Uuid> {
1204 band_targets(&fight, &caster, cap)
1205 .iter()
1206 .map(|e| e.id)
1207 .collect()
1208 };
1209 assert_eq!(ids(None).len(), 5);
1211 assert_eq!(
1213 ids(Some(3)),
1214 vec![
1215 uuid::Uuid::from_u128(1),
1216 uuid::Uuid::from_u128(2),
1217 uuid::Uuid::from_u128(3)
1218 ]
1219 );
1220 }
1221
1222 #[test]
1225 fn excludes_out_of_band_and_allies() {
1226 let caster = Entity {
1227 team: EntityTeam::Ally,
1228 coordinates: Coordinates { x: 0, y: 0 },
1229 ..Default::default()
1230 };
1231 let ally_in_band = Entity {
1232 id: uuid::Uuid::from_u128(3),
1233 team: EntityTeam::Ally,
1234 coordinates: Coordinates { x: 2, y: 0 },
1235 ..Default::default()
1236 };
1237 let fight = ActiveFight {
1238 entities: vec![
1239 enemy_at(1, 1), enemy_at(2, 4), ally_in_band, enemy_at(4, 0), ],
1244 ..Default::default()
1245 };
1246 let got: Vec<uuid::Uuid> = band_targets(&fight, &caster, Some(10))
1247 .iter()
1248 .map(|e| e.id)
1249 .collect();
1250 assert_eq!(got, vec![uuid::Uuid::from_u128(1)]);
1251 }
1252}