veloren_common/states/
basic_summon.rs

1use crate::{
2    combat::{AttackTarget, CombatEffect},
3    comp::{
4        self, Behavior, BehaviorCapability, Body, CharacterState, Object, Ori, PidController, Pos,
5        Projectile, StateUpdate, Stats, Vel,
6        ability::Dodgeable,
7        agent, beam,
8        character_state::OutputEvents,
9        inventory::loadout_builder::{self, LoadoutBuilder},
10        object::{self, Body::FieryTornado},
11    },
12    event::{CreateNpcEvent, CreateObjectEvent, LocalEvent, NpcBuilder, SummonBeamPillarsEvent},
13    npc::NPC_NAMES,
14    outcome::Outcome,
15    resources::Secs,
16    skillset_builder::{self, SkillSetBuilder},
17    states::{
18        behavior::{CharacterBehavior, JoinData},
19        utils::*,
20    },
21    terrain::Block,
22    util::Dir,
23    vol::ReadVol,
24};
25use common_i18n::Content;
26use rand::Rng;
27use serde::{Deserialize, Serialize};
28use std::{
29    f32::consts::{PI, TAU},
30    ops::Sub,
31    time::Duration,
32};
33use vek::*;
34
35/// Separated out to condense update portions of character state
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37pub struct StaticData {
38    /// How long the state builds up for
39    pub buildup_duration: Duration,
40    /// How long the state is casting for
41    pub cast_duration: Duration,
42    /// How long the state recovers for
43    pub recover_duration: Duration,
44    /// Information about the summoned entities
45    pub summon_info: SummonInfo,
46    /// Adjusts move speed during the attack per stage
47    pub movement_modifier: MovementModifier,
48    /// Adjusts turning rate during the attack per stage
49    pub ori_modifier: OrientationModifier,
50    /// Miscellaneous information about the ability
51    pub ability_info: AbilityInfo,
52}
53
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
55pub struct Data {
56    /// Struct containing data that does not change over the course of the
57    /// character state
58    pub static_data: StaticData,
59    /// How many entities have been summoned
60    pub summon_count: u32,
61    /// Timer for each stage
62    pub timer: Duration,
63    /// What section the character stage is in
64    pub stage_section: StageSection,
65    /// Adjusts move speed during the attack
66    pub movement_modifier: Option<f32>,
67    /// How fast the entity should turn
68    pub ori_modifier: Option<f32>,
69}
70
71impl CharacterBehavior for Data {
72    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
73        let mut update = StateUpdate::from(data);
74
75        let target_uid = || {
76            data.controller
77                .queued_inputs
78                .get(&self.static_data.ability_info.input)
79                .or(self.static_data.ability_info.input_attr.as_ref())
80                .and_then(|input| input.target_entity)
81        };
82
83        match self.stage_section {
84            StageSection::Buildup => {
85                if self.timer < self.static_data.buildup_duration {
86                    // Build up
87                    if let CharacterState::BasicSummon(c) = &mut update.character {
88                        c.timer = tick_attack_or_default(data, self.timer, None);
89                    }
90                } else {
91                    // Transitions to recover section of stage
92                    if let CharacterState::BasicSummon(c) = &mut update.character {
93                        c.timer = Duration::default();
94                        c.stage_section = StageSection::Action;
95                        c.movement_modifier = self.static_data.movement_modifier.swing;
96                        c.ori_modifier = self.static_data.ori_modifier.swing;
97                    }
98                }
99            },
100            StageSection::Action => {
101                let summon_amount = self.static_data.summon_info.summon_amount();
102                if self.timer < self.static_data.cast_duration || self.summon_count < summon_amount
103                {
104                    if self.timer
105                        > self.static_data.cast_duration * self.summon_count / summon_amount
106                    {
107                        match &self.static_data.summon_info {
108                            SummonInfo::Npc {
109                                summoned_amount: _,
110                                summon_distance,
111                                body,
112                                loadout_config,
113                                skillset_config,
114                                scale,
115                                has_health,
116                                use_npc_name,
117                                duration,
118                            } => {
119                                let loadout = {
120                                    let loadout_builder =
121                                        LoadoutBuilder::empty().with_default_maintool(body);
122                                    // If preset is none, use default equipment
123                                    if let Some(preset) = loadout_config {
124                                        loadout_builder.with_preset(*preset).build()
125                                    } else {
126                                        loadout_builder.with_default_equipment(body).build()
127                                    }
128                                };
129
130                                let skill_set = {
131                                    let skillset_builder = SkillSetBuilder::default();
132                                    if let Some(preset) = skillset_config {
133                                        skillset_builder.with_preset(*preset).build()
134                                    } else {
135                                        skillset_builder.build()
136                                    }
137                                };
138
139                                let stats = comp::Stats::new(
140                                    use_npc_name
141                                        .then(|| {
142                                            let all_names = NPC_NAMES.read();
143                                            all_names.get_default_name(body)
144                                        })
145                                        .flatten()
146                                        .unwrap_or_else(|| {
147                                            Content::with_attr(
148                                                "name-custom-fallback-summon",
149                                                body.gender_attr(),
150                                            )
151                                        }),
152                                    *body,
153                                );
154
155                                let health = has_health.then(|| comp::Health::new(*body));
156
157                                // Ray cast to check where summon should happen
158                                let summon_frac = self.summon_count as f32 / summon_amount as f32;
159
160                                let length =
161                                    rand::rng().random_range(summon_distance.0..=summon_distance.1);
162                                let extra_height = if *body == Body::Object(FieryTornado) {
163                                    15.0
164                                } else {
165                                    0.0
166                                };
167                                let position = Vec3::new(
168                                    data.pos.0.x,
169                                    data.pos.0.y,
170                                    data.pos.0.z + extra_height,
171                                );
172                                // Summon in a clockwise fashion
173                                let ray_vector = Vec3::new(
174                                    (summon_frac * 2.0 * PI).sin() * length,
175                                    (summon_frac * 2.0 * PI).cos() * length,
176                                    0.0,
177                                );
178
179                                // Check for collision on the xy plane, subtract 1 to get point
180                                // before block
181                                let obstacle_xy = data
182                                    .terrain
183                                    .ray(position, position + length * ray_vector)
184                                    .until(Block::is_solid)
185                                    .cast()
186                                    .0
187                                    .sub(1.0);
188
189                                let collision_vector = Vec3::new(
190                                    position.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy,
191                                    position.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy,
192                                    position.z
193                                        + data.body.eye_height(data.scale.map_or(1.0, |s| s.0)),
194                                );
195
196                                // Check for collision in z up to 50 blocks
197                                let obstacle_z = data
198                                    .terrain
199                                    .ray(collision_vector, collision_vector - Vec3::unit_z() * 50.0)
200                                    .until(Block::is_solid)
201                                    .cast()
202                                    .0;
203
204                                // If a duration is specified, create a projectile component for the
205                                // npc
206                                let projectile = duration.map(|duration| Projectile {
207                                    hit_solid: Vec::new(),
208                                    hit_entity: Vec::new(),
209                                    timeout: Vec::new(),
210                                    time_left: duration,
211                                    init_time: Secs(duration.as_secs_f64()),
212                                    owner: Some(*data.uid),
213                                    ignore_group: true,
214                                    is_sticky: false,
215                                    is_point: false,
216                                    homing: None,
217                                    pierce_entities: false,
218                                    hit_entities: Vec::new(),
219                                    limit_per_ability: false,
220                                    override_collider: None,
221                                });
222
223                                let mut rng = rand::rng();
224                                // Send server event to create npc
225                                output_events.emit_server(CreateNpcEvent {
226                                    pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
227                                    ori: comp::Ori::from(Dir::random_2d(&mut rng)),
228                                    npc: NpcBuilder::new(
229                                        stats,
230                                        *body,
231                                        comp::Alignment::Owned(*data.uid),
232                                    )
233                                    .with_skill_set(skill_set)
234                                    .with_health(health)
235                                    .with_inventory(comp::Inventory::with_loadout(loadout, *body))
236                                    .with_agent(
237                                        comp::Agent::from_body(body)
238                                            .with_behavior(Behavior::from(
239                                                BehaviorCapability::SPEAK,
240                                            ))
241                                            .with_no_flee_if(true),
242                                    )
243                                    .with_scale(scale.unwrap_or(comp::Scale(1.0)))
244                                    .with_projectile(projectile),
245                                });
246
247                                // Send local event used for frontend shenanigans
248                                output_events.emit_local(LocalEvent::CreateOutcome(
249                                    Outcome::SummonedCreature {
250                                        pos: data.pos.0,
251                                        body: *body,
252                                    },
253                                ));
254                            },
255                            SummonInfo::BeamPillar {
256                                buildup_duration,
257                                attack_duration,
258                                beam_duration,
259                                target,
260                                radius,
261                                height,
262                                damage,
263                                damage_effect,
264                                dodgeable,
265                                tick_rate,
266                                specifier,
267                                indicator_specifier,
268                            } => {
269                                let target = match target {
270                                    BeamPillarTarget::Single => target_uid()
271                                        .and_then(|target_uid| data.id_maps.uid_entity(target_uid))
272                                        .map(AttackTarget::Entity),
273                                    BeamPillarTarget::AllInRange(range) => {
274                                        Some(AttackTarget::AllInRange(*range))
275                                    },
276                                };
277
278                                if let Some(target) = target {
279                                    output_events.emit_server(SummonBeamPillarsEvent {
280                                        summoner: data.entity,
281                                        target,
282                                        buildup_duration: Duration::from_secs_f32(
283                                            *buildup_duration,
284                                        ),
285                                        attack_duration: Duration::from_secs_f32(*attack_duration),
286                                        beam_duration: Duration::from_secs_f32(*beam_duration),
287                                        radius: *radius,
288                                        height: *height,
289                                        damage: *damage,
290                                        damage_effect: damage_effect.clone(),
291                                        dodgeable: *dodgeable,
292                                        tick_rate: *tick_rate,
293                                        specifier: *specifier,
294                                        indicator_specifier: *indicator_specifier,
295                                    });
296                                }
297                            },
298                            SummonInfo::BeamWall {
299                                buildup_duration,
300                                attack_duration,
301                                beam_duration,
302                                pillar_count,
303                                wall_radius,
304                                pillar_radius,
305                                height,
306                                damage,
307                                damage_effect,
308                                dodgeable,
309                                tick_rate,
310                                specifier,
311                                indicator_specifier,
312                            } => {
313                                let xy_angle = data
314                                    .ori
315                                    .to_horizontal()
316                                    .angle_between(Ori::from(Dir::right()));
317
318                                let phi = TAU / *pillar_count as f32;
319
320                                output_events.emit_server(SummonBeamPillarsEvent {
321                                    summoner: data.entity,
322                                    target: AttackTarget::Pos(Vec3::new(
323                                        data.pos.0.x
324                                            + (wall_radius
325                                                * (self.summon_count as f32 * phi + xy_angle)
326                                                    .cos()),
327                                        data.pos.0.y
328                                            + (wall_radius
329                                                * (self.summon_count as f32 * phi + xy_angle)
330                                                    .sin()),
331                                        data.pos.0.z,
332                                    )),
333                                    buildup_duration: Duration::from_secs_f32(*buildup_duration),
334                                    attack_duration: Duration::from_secs_f32(*attack_duration),
335                                    beam_duration: Duration::from_secs_f32(*beam_duration),
336                                    radius: *pillar_radius,
337                                    height: *height,
338                                    damage: *damage,
339                                    damage_effect: damage_effect.clone(),
340                                    dodgeable: *dodgeable,
341                                    tick_rate: *tick_rate,
342                                    specifier: *specifier,
343                                    indicator_specifier: *indicator_specifier,
344                                });
345                            },
346                            SummonInfo::Crux {
347                                max_height,
348                                scale,
349                                range,
350                                strength,
351                                duration,
352                            } => {
353                                let body = object::Body::Crux;
354                                if let Some((kp, ki, kd)) =
355                                    agent::pid_coefficients(&Body::Object(body))
356                                {
357                                    let initial_pos = data.pos.0 + 2.0 * Vec3::<f32>::unit_z();
358
359                                    output_events.emit_server(CreateObjectEvent {
360                                        pos: Pos(initial_pos),
361                                        vel: Vel(Vec3::zero()),
362                                        body: object::Body::Crux,
363                                        object: Some(Object::Crux {
364                                            owner: *data.uid,
365                                            scale: *scale,
366                                            range: *range,
367                                            strength: *strength,
368                                            duration: Secs(*duration),
369                                            pid_controller: Some(PidController::new(
370                                                kp,
371                                                ki,
372                                                kd,
373                                                initial_pos.z + max_height,
374                                                0.0,
375                                                |sp, pv| sp - pv,
376                                            )),
377                                        }),
378                                        item: None,
379                                        light_emitter: None,
380                                        stats: Some(Stats::new(
381                                            Content::Key(String::from("lantern-crux")),
382                                            Body::Object(object::Body::Crux),
383                                        )),
384                                    });
385                                }
386                            },
387                        }
388
389                        if let CharacterState::BasicSummon(c) = &mut update.character {
390                            c.timer = tick_attack_or_default(data, self.timer, None);
391                            c.summon_count = self.summon_count + 1;
392                        }
393                    } else {
394                        // Cast
395                        if let CharacterState::BasicSummon(c) = &mut update.character {
396                            c.timer = tick_attack_or_default(data, self.timer, None);
397                        }
398                    }
399                } else {
400                    // Transitions to recover section of stage
401                    if let CharacterState::BasicSummon(c) = &mut update.character {
402                        c.timer = Duration::default();
403                        c.stage_section = StageSection::Recover;
404                        c.movement_modifier = self.static_data.movement_modifier.recover;
405                        c.ori_modifier = self.static_data.ori_modifier.recover;
406                    }
407                }
408            },
409            StageSection::Recover => {
410                if self.timer < self.static_data.recover_duration {
411                    // Recovery
412                    if let CharacterState::BasicSummon(c) = &mut update.character {
413                        c.timer = tick_attack_or_default(
414                            data,
415                            self.timer,
416                            Some(data.stats.recovery_speed_modifier),
417                        );
418                    }
419                } else {
420                    // Done
421                    end_ability(data, &mut update);
422                }
423            },
424            _ => {
425                // If it somehow ends up in an incorrect stage section
426                end_ability(data, &mut update);
427            },
428        }
429
430        update
431    }
432}
433
434#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
435pub enum BeamPillarTarget {
436    Single,
437    AllInRange(f32),
438}
439
440#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
441pub enum SummonInfo {
442    Npc {
443        summoned_amount: u32,
444        summon_distance: (f32, f32),
445        body: comp::Body,
446        scale: Option<comp::Scale>,
447        has_health: bool,
448        #[serde(default)]
449        use_npc_name: bool,
450        // TODO: use assets for specifying skills and loadout?
451        loadout_config: Option<loadout_builder::Preset>,
452        skillset_config: Option<skillset_builder::Preset>,
453        duration: Option<Duration>,
454    },
455    BeamPillar {
456        buildup_duration: f32,
457        attack_duration: f32,
458        beam_duration: f32,
459        target: BeamPillarTarget,
460        radius: f32,
461        height: f32,
462        damage: f32,
463        #[serde(default)]
464        damage_effect: Option<CombatEffect>,
465        #[serde(default)]
466        dodgeable: Dodgeable,
467        tick_rate: f32,
468        specifier: beam::FrontendSpecifier,
469        indicator_specifier: BeamPillarIndicatorSpecifier,
470    },
471    BeamWall {
472        buildup_duration: f32,
473        attack_duration: f32,
474        beam_duration: f32,
475        pillar_count: u32,
476        wall_radius: f32,
477        pillar_radius: f32,
478        height: f32,
479        damage: f32,
480        #[serde(default)]
481        damage_effect: Option<CombatEffect>,
482        #[serde(default)]
483        dodgeable: Dodgeable,
484        tick_rate: f32,
485        specifier: beam::FrontendSpecifier,
486        indicator_specifier: BeamPillarIndicatorSpecifier,
487    },
488    Crux {
489        max_height: f32,
490        scale: f32,
491        range: f32,
492        strength: f32,
493        duration: f64,
494    },
495}
496
497impl SummonInfo {
498    fn summon_amount(&self) -> u32 {
499        match self {
500            SummonInfo::Npc {
501                summoned_amount, ..
502            } => *summoned_amount,
503            SummonInfo::BeamPillar { .. } => 1, // Fire pillars are summoned simultaneously
504            SummonInfo::BeamWall { pillar_count, .. } => *pillar_count,
505            SummonInfo::Crux { .. } => 1,
506        }
507    }
508
509    pub fn scale_range(&mut self, scale: f32) {
510        match self {
511            SummonInfo::Npc {
512                summon_distance, ..
513            } => {
514                summon_distance.0 *= scale;
515                summon_distance.1 *= scale;
516            },
517            SummonInfo::BeamPillar { target, .. } => {
518                if let BeamPillarTarget::AllInRange(range) = target {
519                    *range *= scale;
520                }
521            },
522            SummonInfo::BeamWall { wall_radius, .. } => {
523                *wall_radius *= scale;
524            },
525            SummonInfo::Crux { .. } => {},
526        }
527    }
528}
529
530#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, strum::EnumString)]
531pub enum BeamPillarIndicatorSpecifier {
532    FirePillar,
533}