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