overlord_event_system/behaviors/combat/
cast_projectile.rs

1//! Native ports for the `cast_projectile` category — projectile `script`s (the
2//! `CastProjectile` event handler, run via `run_event` in
3//! `logic::fighting::handle_cast_projectile`, returning
4//! `Vec<OverlordEvent>`).
5//!
6//! Every shipped projectile `script` is `ctx.attack(CasterEntity, TargetEntity,
7//! #{ ... })` with a per-projectile param map, resolved from a sibling ability's
8//! `get_ability_info(<ability_id>, ProjectileLevel)`:
9//!
10//! * plain damage: `#{ power: ability_info.damage }`
11//! * DoT split: `#{ power: ability_info.damage, dot_power: ability_info.dot }`
12//! * vampiric: `attack{power}`, then `if (damage > 0) heal_entity(caster, damage * vampiric)`
13//! * counterattack: `#{ power: balance::COUNTERATTACK_POWER, no_counterattack: true }`
14//!
15//! ## RNG
16//! `attack` consumes the authoritative `Random` (evasion / counterattack /
17//! deceit / crit / block throws).
18//!
19//! ## Scope
20//! `CasterEntity`, `TargetEntity`, `Fight`, `Random`, `ProjectileLevel`,
21//! `CustomEventData`, `CurrentTick`, `FightDurationTicks`, the event.
22
23use configs::game_config::GameConfig;
24use essences::combat_origin::CombatEventOrigin;
25use essences::entity::Entity;
26use essences::fight_breakdown::CombatSource;
27use essences::fighting::ActiveFight;
28use event_system::script::random::GameRng;
29use uuid::Uuid;
30
31use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
32use crate::event::OverlordEvent;
33use crate::mechanics::balance;
34use crate::mechanics::content::{AbilityInfo, ability_info};
35use crate::mechanics::content_lookups::ContentLookups;
36use crate::mechanics::fight::{AttackParams, NativeSink, heal_entity, stat_throw};
37use crate::mechanics::support_shapes::{SupportAttackCtx, attack_with_supports};
38use essences::ability_stones::AbilityStoneMods;
39
40use super::cast_ability::band_targets;
41
42/// Inputs available to a `cast_projectile` native fn.
43pub struct CastProjectileCtx<'a> {
44    /// `CasterEntity` const.
45    pub caster_entity: &'a Entity,
46    /// `TargetEntity` const.
47    pub target_entity: &'a Entity,
48    /// `Fight` const (for `player_id` in the attack screen-shake branch).
49    pub fight: &'a ActiveFight,
50    /// RNG snapshot (clone at the same state).
51    pub rng: &'a GameRng,
52    /// `ProjectileLevel` const (passed to `get_ability_info`).
53    pub projectile_level: i64,
54    pub config: &'a GameConfig,
55    pub lookups: &'a ContentLookups,
56    /// Ability stones active on the caster (the projectile carries the parent
57    /// ability's damage, so it carries the parent ability's stones too).
58    pub stones: crate::mechanics::ability_stones::AbilityStoneResolver<'a>,
59    /// Breakdown attribution of everything this projectile lands. Resolved by
60    /// the dispatcher from the projectile id, so the counterattack reflex reads
61    /// as a counterattack rather than as an anonymous projectile.
62    pub source: CombatSource,
63}
64
65/// Signature of a `cast_projectile` native fn.
66pub type CastProjectileFn = fn(&CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>>;
67
68/// `content::get_ability_info(id, ProjectileLevel)` plus the ability-stone
69/// modifiers of the ability this projectile carries (already folded into the
70/// returned info).
71fn info(
72    ctx: &CastProjectileCtx,
73    ability_id: &str,
74) -> anyhow::Result<(AbilityInfo, AbilityStoneMods)> {
75    let id = Uuid::parse_str(ability_id).expect("valid ability uuid literal");
76    let mut info = ability_info(ctx.config, ctx.lookups, id, ctx.projectile_level)?;
77    let mods = ctx.stones.mods_for(ctx.config, id, ctx.projectile_level);
78    info.apply_stone_mods(&mods);
79    Ok((info, mods))
80}
81
82/// Run `ctx.attack(caster, target, params)` into a sink through the support
83/// seam (crit bonus + Split/Chain/Pierce copies + Leech), returning the floored
84/// damage of the PRIMARY hit.
85fn run_attack_on(
86    ctx: &CastProjectileCtx,
87    sink: &mut NativeSink,
88    mods: &AbilityStoneMods,
89    target: &Entity,
90    params: &AttackParams,
91) -> anyhow::Result<Option<i64>> {
92    attack_with_supports(
93        sink,
94        &SupportAttackCtx {
95            rng: ctx.rng,
96            lookups: ctx.lookups,
97            fight: ctx.fight,
98            player_id: ctx.fight.player_id,
99            caster: ctx.caster_entity,
100            mods,
101            source: ctx.source,
102        },
103        target,
104        params,
105    )
106}
107
108/// The common case: the projectile hits the entity it was aimed at.
109fn run_attack(
110    ctx: &CastProjectileCtx,
111    sink: &mut NativeSink,
112    mods: &AbilityStoneMods,
113    params: &AttackParams,
114) -> anyhow::Result<Option<i64>> {
115    run_attack_on(ctx, sink, mods, ctx.target_entity, params)
116}
117
118/// Shared body: `attack(caster, target, #{ power: ability_info(ability_id).damage })`.
119fn attack_power(ctx: &CastProjectileCtx, ability_id: &str) -> anyhow::Result<Vec<OverlordEvent>> {
120    let mut sink = NativeSink::default();
121    let (ability_info, mods) = info(ctx, ability_id)?;
122    let params = AttackParams {
123        power: ability_info.damage,
124        ..Default::default()
125    };
126    run_attack(ctx, &mut sink, &mods, &params)?;
127    Ok(sink.events)
128}
129
130/// `01966cbc-...` — `attack(#{ power: damage, dot_power: dot })`, info from
131/// ability `01958172`.
132pub fn attack_power_dot(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
133    let mut sink = NativeSink::default();
134    let (ability_info, mods) = info(ctx, "01958172-9e65-7061-9d15-56b2c33cc13e")?;
135    let params = AttackParams {
136        power: ability_info.damage,
137        dot_power: ability_info.dot,
138        ..Default::default()
139    };
140    run_attack(ctx, &mut sink, &mods, &params)?;
141    Ok(sink.events)
142}
143
144/// `0196a6b3-...` — `attack(#{ power })`, info from ability `019589e6`.
145pub fn arcane_missiles_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
146    attack_power(ctx, "019589e6-f9dd-7b22-8d39-5350e95aaf69")
147}
148
149/// `019a0244-4675-...` — `attack(#{ power })`, info from ability `019a0245`.
150pub fn mushroom_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
151    attack_power(ctx, "019a0245-ff0c-7964-b345-ded525c71e74")
152}
153
154/// `019a0244-7a0a-...` — `attack(#{ power })`, info from ability `019a0246-5aaf`.
155pub fn chipmunk_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
156    attack_power(ctx, "019a0246-5aaf-7c01-9a1a-3969e04129ec")
157}
158
159/// `019a0244-aad4-...` — `attack(#{ power })`, info from ability `019a0246-cf87`.
160pub fn bat_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
161    attack_power(ctx, "019a0246-cf87-73b0-b701-f8788cf9cc08")
162}
163
164/// `019c009c-...` — `attack(#{ power })`, info from ability `019c00a4`.
165pub fn shot_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
166    attack_power(ctx, "019c00a4-38c9-7859-a642-fd82c25ef285")
167}
168
169/// `0196a6b4-...` — vampiric: `attack(#{ power })`; if damage dealt (`> 0` in
170///
171/// Fidelity note: when `attack` returns nothing (evasion / no-damage), there is
172/// no damage to read, so this slot must emit **zero** events. We return `Err`
173/// (discarding the partial sink) on the `None` branch so the handler produces
174/// no events.
175pub fn attack_vampiric(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
176    let mut sink = NativeSink::default();
177    let (ability_info, mods) = info(ctx, "019589f7-f4a3-701c-bc0f-f60d980ae250")?;
178    let params = AttackParams {
179        power: ability_info.damage,
180        ..Default::default()
181    };
182    let damage = run_attack(ctx, &mut sink, &mods, &params)?;
183    let Some(damage) = damage else {
184        anyhow::bail!("attack returned no damage; the `damage > 0` contract failed");
185    };
186    if damage > 0 {
187        let vampiric = ability_info.vampiric.unwrap_or(0.0);
188        heal_entity(
189            &mut sink,
190            Some(ctx.caster_entity.id),
191            ctx.caster_entity,
192            damage as f64 * vampiric,
193            ctx.source,
194        )
195        .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
196    }
197    Ok(sink.events)
198}
199
200/// `019aeeed-...` — counterattack: `attack(#{ power: COUNTERATTACK_POWER,
201/// no_counterattack: true })`. No `get_ability_info`.
202pub fn counterattack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
203    let mut sink = NativeSink::default();
204    let params = AttackParams {
205        power: Some(balance::COUNTERATTACK_POWER),
206        no_counterattack: true,
207        ..Default::default()
208    };
209    // A counterattack is the TARGET's reflex, not the caster's ability: it
210    // carries no ability payload, so no support stone touches it.
211    run_attack(ctx, &mut sink, &AbilityStoneMods::identity(), &params)?;
212    Ok(sink.events)
213}
214
215/// `79f90288-...` — pet Night Pounce delivery: `attack(#{ power,
216/// crit_chance_bonus })`, info from ability `019584aa` (the Strike the pet ult
217/// clones). Same math as the instant `ability_crit_strike`, moved onto a
218/// projectile so the cast is readable in combat.
219pub fn pet_night_pounce_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
220    let mut sink = NativeSink::default();
221    let (ability_info, mods) = info(ctx, "019584aa-5bde-7ac2-8850-076dafdc4603")?;
222    let params = AttackParams {
223        power: ability_info.damage,
224        crit_chance_bonus: ability_info.crit_chance_bonus.unwrap_or(0.0) * 10000.0,
225        ..Default::default()
226    };
227    run_attack(ctx, &mut sink, &mods, &params)?;
228    Ok(sink.events)
229}
230
231/// `1ce4597d-...` — pet Rust Wave delivery: one shared crit throw, then attack
232/// every enemy in the caster's forward 3-cell x-band with `{ power, is_crit }`,
233/// info from ability `019584f4` (the Nova the pet ult clones). Same math as the
234/// instant `ability_cone_strike`; the cone resolves on projectile arrival.
235pub fn pet_rust_wave_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
236    let mut sink = NativeSink::default();
237    let (ability_info, mods) = info(ctx, "019584f4-2c99-72bf-bcd3-bfc02fb33977")?;
238    let is_crit = stat_throw(ctx.rng, ctx.lookups, ctx.caster_entity, "crit_chance", 0.0);
239    let targets: Vec<uuid::Uuid> =
240        band_targets(ctx.fight, ctx.caster_entity, ability_info.max_targets)
241            .iter()
242            .map(|e| e.id)
243            .collect();
244    for target_id in targets {
245        let Some(target) = ctx.fight.entities.iter().find(|e| e.id == target_id) else {
246            continue;
247        };
248        let params = AttackParams {
249            power: ability_info.damage,
250            is_crit: Some(is_crit),
251            ..Default::default()
252        };
253        run_attack_on(ctx, &mut sink, &mods, target, &params)?;
254    }
255    Ok(sink.events)
256}
257
258/// `432f231c-...` — pet Odd Flame delivery: `attack(#{ dot_power })`, info from
259/// ability `01958efd` (the Immolate the pet ult clones). Same math as the
260/// instant `ability_dot_strike`, moved onto a projectile.
261pub fn pet_odd_flame_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
262    let mut sink = NativeSink::default();
263    let (ability_info, mods) = info(ctx, "01958efd-77f9-7dec-8444-c9d759549225")?;
264    let params = AttackParams {
265        dot_power: ability_info.damage,
266        ..Default::default()
267    };
268    run_attack(ctx, &mut sink, &mods, &params)?;
269    Ok(sink.events)
270}
271
272/// Port of the test projectile `script`
273/// `Result.push(OverlordEventDamage(TargetEntity.id, unsigned(5), CustomEventData()));`
274/// — raw 5 damage to the target. RNG-free; used by `test_fighting_abilities`.
275pub fn damage_target_5(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
276    Ok(vec![OverlordEvent::Damage {
277        by_entity_id: Some(ctx.caster_entity.id),
278        entity_id: ctx.target_entity.id,
279        damage: 5,
280        damage_data: crate::event::CustomEventData::default(),
281        origin: CombatEventOrigin::Core,
282        source: ctx.source,
283    }])
284}
285
286/// Register this category's native fns.
287pub fn register(registry: &mut BehaviorRegistry) {
288    let mut reg = |name: &str, title: &str, desc: &str, f: CastProjectileFn| {
289        registry.register_cast_projectile(
290            BehaviorMeta {
291                name: name.to_string(),
292                category: BehaviorKind::CastProjectile,
293                title: title.to_string(),
294                description: desc.to_string(),
295            },
296            f,
297        );
298    };
299
300    reg(
301        "projectile_damage_target_5",
302        "Снаряд: урон 5 по цели (тест)",
303        "Damage(TargetEntity, 5) — порт test projectile script.",
304        damage_target_5,
305    );
306    reg(
307        "projectile_attack_power_dot",
308        "Снаряд: attack{power,dot} (01966cbc)",
309        "attack(#{power: ability_info.damage, dot_power: ability_info.dot}) \
310         info из 01958172 (порт projectile script 01966cbc).",
311        attack_power_dot,
312    );
313    reg(
314        "projectile_arcane_missiles_attack",
315        "Снаряд: attack{power} (0196a6b3)",
316        "attack(#{power: ability_info.damage}) info из 019589e6 (порт projectile script 0196a6b3).",
317        arcane_missiles_attack,
318    );
319    reg(
320        "projectile_mushroom_attack",
321        "Снаряд: attack{power} (019a0244-4675)",
322        "attack(#{power: ability_info.damage}) info из 019a0245 (порт projectile script 019a0244-4675).",
323        mushroom_attack,
324    );
325    reg(
326        "projectile_chipmunk_attack",
327        "Снаряд: attack{power} (019a0244-7a0a)",
328        "attack(#{power: ability_info.damage}) info из 019a0246-5aaf (порт projectile script 019a0244-7a0a).",
329        chipmunk_attack,
330    );
331    reg(
332        "projectile_bat_attack",
333        "Снаряд: attack{power} (019a0244-aad4)",
334        "attack(#{power: ability_info.damage}) info из 019a0246-cf87 (порт projectile script 019a0244-aad4).",
335        bat_attack,
336    );
337    reg(
338        "projectile_shot_attack",
339        "Снаряд: attack{power} (019c009c)",
340        "attack(#{power: ability_info.damage}) info из 019c00a4 (порт projectile script 019c009c).",
341        shot_attack,
342    );
343    reg(
344        "projectile_attack_vampiric",
345        "Снаряд: вампиризм (0196a6b4)",
346        "attack(#{power}); при уроне>0 heal_entity(Caster, damage*vampiric) \
347         info из 019589f7 (порт projectile script 0196a6b4).",
348        attack_vampiric,
349    );
350    reg(
351        "projectile_pet_night_pounce",
352        "Снаряд: удар с бонусом крита (79f90288, ульта питомца Nyx)",
353        "attack(#{power, crit_chance_bonus}) info из 019584aa — та же математика, \
354         что у ability_crit_strike, но доставка снарядом.",
355        pet_night_pounce_attack,
356    );
357    reg(
358        "projectile_pet_rust_wave",
359        "Снаряд: конус по фронтальным врагам (1ce4597d, ульта питомца Rusty)",
360        "Один общий crit-бросок, attack по врагам в 3-клеточной зоне по x при \
361         прилёте снаряда; info из 019584f4 — та же математика, что у ability_cone_strike.",
362        pet_rust_wave_attack,
363    );
364    reg(
365        "projectile_pet_odd_flame",
366        "Снаряд: чистый DoT-удар (432f231c, ульта питомца Thingy)",
367        "attack(#{dot_power}) info из 01958efd — та же математика, что у \
368         ability_dot_strike, но доставка снарядом.",
369        pet_odd_flame_attack,
370    );
371    reg(
372        "projectile_counterattack",
373        "Снаряд: контратака (019aeeed)",
374        "attack(#{power: COUNTERATTACK_POWER, no_counterattack: true}) \
375         (порт projectile script 019aeeed).",
376        counterattack,
377    );
378}