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::entity::Entity;
25use essences::fighting::ActiveFight;
26use event_system::script::random::GameRng;
27use uuid::Uuid;
28
29use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
30use crate::event::OverlordEvent;
31use crate::mechanics::balance;
32use crate::mechanics::content::{AbilityInfo, ability_info};
33use crate::mechanics::content_lookups::ContentLookups;
34use crate::mechanics::effect_cb::OverlordEffectCb;
35use crate::mechanics::fight::{AttackParams, NativeSink, attack, heal_entity};
36
37/// Inputs available to a `cast_projectile` native fn.
38pub struct CastProjectileCtx<'a> {
39    /// `CasterEntity` const.
40    pub caster_entity: &'a Entity,
41    /// `TargetEntity` const.
42    pub target_entity: &'a Entity,
43    /// `Fight` const (for `player_id` in the attack screen-shake branch).
44    pub fight: &'a ActiveFight,
45    /// RNG snapshot (clone at the same state).
46    pub rng: &'a GameRng,
47    /// `ProjectileLevel` const (passed to `get_ability_info`).
48    pub projectile_level: i64,
49    pub config: &'a GameConfig,
50    pub lookups: &'a ContentLookups,
51}
52
53/// Signature of a `cast_projectile` native fn.
54pub type CastProjectileFn = fn(&CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>>;
55
56/// `content::get_ability_info(id, ProjectileLevel)`.
57fn info(ctx: &CastProjectileCtx, ability_id: &str) -> anyhow::Result<AbilityInfo> {
58    let id = Uuid::parse_str(ability_id).expect("valid ability uuid literal");
59    ability_info(ctx.config, ctx.lookups, id, ctx.projectile_level)
60}
61
62/// Run `ctx.attack(caster, target, params)` into a sink, returning floored
63fn run_attack(
64    ctx: &CastProjectileCtx,
65    sink: &mut NativeSink,
66    params: &AttackParams,
67) -> anyhow::Result<Option<i64>> {
68    let mut effects = OverlordEffectCb;
69    attack(
70        sink,
71        ctx.rng,
72        ctx.lookups,
73        &mut effects,
74        ctx.fight.player_id,
75        ctx.caster_entity,
76        ctx.target_entity,
77        params,
78    )
79    .map_err(|e| anyhow::anyhow!("attack: {e}"))
80}
81
82/// Shared body: `attack(caster, target, #{ power: ability_info(ability_id).damage })`.
83fn attack_power(ctx: &CastProjectileCtx, ability_id: &str) -> anyhow::Result<Vec<OverlordEvent>> {
84    let mut sink = NativeSink::default();
85    let ability_info = info(ctx, ability_id)?;
86    let params = AttackParams {
87        power: ability_info.damage,
88        ..Default::default()
89    };
90    run_attack(ctx, &mut sink, &params)?;
91    Ok(sink.events)
92}
93
94/// `01966cbc-...` — `attack(#{ power: damage, dot_power: dot })`, info from
95/// ability `01958172`.
96pub fn attack_power_dot(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
97    let mut sink = NativeSink::default();
98    let ability_info = info(ctx, "01958172-9e65-7061-9d15-56b2c33cc13e")?;
99    let params = AttackParams {
100        power: ability_info.damage,
101        dot_power: ability_info.dot,
102        ..Default::default()
103    };
104    run_attack(ctx, &mut sink, &params)?;
105    Ok(sink.events)
106}
107
108/// `0196a6b3-...` — `attack(#{ power })`, info from ability `019589e6`.
109pub fn arcane_missiles_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
110    attack_power(ctx, "019589e6-f9dd-7b22-8d39-5350e95aaf69")
111}
112
113/// `019a0244-4675-...` — `attack(#{ power })`, info from ability `019a0245`.
114pub fn mushroom_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
115    attack_power(ctx, "019a0245-ff0c-7964-b345-ded525c71e74")
116}
117
118/// `019a0244-7a0a-...` — `attack(#{ power })`, info from ability `019a0246-5aaf`.
119pub fn chipmunk_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
120    attack_power(ctx, "019a0246-5aaf-7c01-9a1a-3969e04129ec")
121}
122
123/// `019a0244-aad4-...` — `attack(#{ power })`, info from ability `019a0246-cf87`.
124pub fn bat_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
125    attack_power(ctx, "019a0246-cf87-73b0-b701-f8788cf9cc08")
126}
127
128/// `019c009c-...` — `attack(#{ power })`, info from ability `019c00a4`.
129pub fn shot_attack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
130    attack_power(ctx, "019c00a4-38c9-7859-a642-fd82c25ef285")
131}
132
133/// `0196a6b4-...` — vampiric: `attack(#{ power })`; if damage dealt (`> 0` in
134///
135/// Fidelity note: when `attack` returns nothing (evasion / no-damage), there is
136/// no damage to read, so this slot must emit **zero** events. We return `Err`
137/// (discarding the partial sink) on the `None` branch so the handler produces
138/// no events.
139pub fn attack_vampiric(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
140    let mut sink = NativeSink::default();
141    let ability_info = info(ctx, "019589f7-f4a3-701c-bc0f-f60d980ae250")?;
142    let params = AttackParams {
143        power: ability_info.damage,
144        ..Default::default()
145    };
146    let damage = run_attack(ctx, &mut sink, &params)?;
147    let Some(damage) = damage else {
148        anyhow::bail!("attack returned no damage; the `damage > 0` contract failed");
149    };
150    if damage > 0 {
151        let vampiric = ability_info.vampiric.unwrap_or(0.0);
152        heal_entity(&mut sink, ctx.caster_entity, damage as f64 * vampiric)
153            .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
154    }
155    Ok(sink.events)
156}
157
158/// `019aeeed-...` — counterattack: `attack(#{ power: COUNTERATTACK_POWER,
159/// no_counterattack: true })`. No `get_ability_info`.
160pub fn counterattack(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
161    let mut sink = NativeSink::default();
162    let params = AttackParams {
163        power: Some(balance::COUNTERATTACK_POWER),
164        no_counterattack: true,
165        ..Default::default()
166    };
167    run_attack(ctx, &mut sink, &params)?;
168    Ok(sink.events)
169}
170
171/// Port of the test projectile `script`
172/// `Result.push(OverlordEventDamage(TargetEntity.id, unsigned(5), CustomEventData()));`
173/// — raw 5 damage to the target. RNG-free; used by `test_fighting_abilities`.
174pub fn damage_target_5(ctx: &CastProjectileCtx) -> anyhow::Result<Vec<OverlordEvent>> {
175    Ok(vec![OverlordEvent::Damage {
176        entity_id: ctx.target_entity.id,
177        damage: 5,
178        damage_data: crate::event::CustomEventData::default(),
179    }])
180}
181
182/// Register this category's native fns.
183pub fn register(registry: &mut BehaviorRegistry) {
184    let mut reg = |name: &str, title: &str, desc: &str, f: CastProjectileFn| {
185        registry.register_cast_projectile(
186            BehaviorMeta {
187                name: name.to_string(),
188                category: BehaviorKind::CastProjectile,
189                title: title.to_string(),
190                description: desc.to_string(),
191            },
192            f,
193        );
194    };
195
196    reg(
197        "projectile_damage_target_5",
198        "Снаряд: урон 5 по цели (тест)",
199        "Damage(TargetEntity, 5) — порт test projectile script.",
200        damage_target_5,
201    );
202    reg(
203        "projectile_attack_power_dot",
204        "Снаряд: attack{power,dot} (01966cbc)",
205        "attack(#{power: ability_info.damage, dot_power: ability_info.dot}) \
206         info из 01958172 (порт projectile script 01966cbc).",
207        attack_power_dot,
208    );
209    reg(
210        "projectile_arcane_missiles_attack",
211        "Снаряд: attack{power} (0196a6b3)",
212        "attack(#{power: ability_info.damage}) info из 019589e6 (порт projectile script 0196a6b3).",
213        arcane_missiles_attack,
214    );
215    reg(
216        "projectile_mushroom_attack",
217        "Снаряд: attack{power} (019a0244-4675)",
218        "attack(#{power: ability_info.damage}) info из 019a0245 (порт projectile script 019a0244-4675).",
219        mushroom_attack,
220    );
221    reg(
222        "projectile_chipmunk_attack",
223        "Снаряд: attack{power} (019a0244-7a0a)",
224        "attack(#{power: ability_info.damage}) info из 019a0246-5aaf (порт projectile script 019a0244-7a0a).",
225        chipmunk_attack,
226    );
227    reg(
228        "projectile_bat_attack",
229        "Снаряд: attack{power} (019a0244-aad4)",
230        "attack(#{power: ability_info.damage}) info из 019a0246-cf87 (порт projectile script 019a0244-aad4).",
231        bat_attack,
232    );
233    reg(
234        "projectile_shot_attack",
235        "Снаряд: attack{power} (019c009c)",
236        "attack(#{power: ability_info.damage}) info из 019c00a4 (порт projectile script 019c009c).",
237        shot_attack,
238    );
239    reg(
240        "projectile_attack_vampiric",
241        "Снаряд: вампиризм (0196a6b4)",
242        "attack(#{power}); при уроне>0 heal_entity(Caster, damage*vampiric) \
243         info из 019589f7 (порт projectile script 0196a6b4).",
244        attack_vampiric,
245    );
246    reg(
247        "projectile_counterattack",
248        "Снаряд: контратака (019aeeed)",
249        "attack(#{power: COUNTERATTACK_POWER, no_counterattack: true}) \
250         (порт projectile script 019aeeed).",
251        counterattack,
252    );
253}