veloren_common/states/
basic_summon.rs

1use crate::{
2    comp::{
3        self, Behavior, BehaviorCapability,
4        Body::Object,
5        CharacterState, Projectile, StateUpdate,
6        character_state::OutputEvents,
7        inventory::loadout_builder::{self, LoadoutBuilder},
8        object::Body::FieryTornado,
9    },
10    event::{CreateNpcEvent, LocalEvent, NpcBuilder},
11    npc::NPC_NAMES,
12    outcome::Outcome,
13    skillset_builder::{self, SkillSetBuilder},
14    states::{
15        behavior::{CharacterBehavior, JoinData},
16        utils::*,
17    },
18    terrain::Block,
19    util::Dir,
20    vol::ReadVol,
21};
22use rand::Rng;
23use serde::{Deserialize, Serialize};
24use std::{f32::consts::PI, ops::Sub, time::Duration};
25use vek::*;
26
27/// Separated out to condense update portions of character state
28#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct StaticData {
30    /// How long the state builds up for
31    pub buildup_duration: Duration,
32    /// How long the state is casting for
33    pub cast_duration: Duration,
34    /// How long the state recovers for
35    pub recover_duration: Duration,
36    /// How many creatures the state should summon
37    pub summon_amount: u32,
38    /// Range of the summons relative to the summoner
39    pub summon_distance: (f32, f32),
40    /// Information about the summoned creature
41    pub summon_info: SummonInfo,
42    /// Miscellaneous information about the ability
43    pub ability_info: AbilityInfo,
44    /// Duration of the summoned entity
45    pub duration: Option<Duration>,
46}
47
48#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct Data {
50    /// Struct containing data that does not change over the course of the
51    /// character state
52    pub static_data: StaticData,
53    /// How many creatures have been summoned
54    pub summon_count: u32,
55    /// Timer for each stage
56    pub timer: Duration,
57    /// What section the character stage is in
58    pub stage_section: StageSection,
59}
60
61impl CharacterBehavior for Data {
62    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
63        let mut update = StateUpdate::from(data);
64
65        match self.stage_section {
66            StageSection::Buildup => {
67                if self.timer < self.static_data.buildup_duration {
68                    // Build up
69                    update.character = CharacterState::BasicSummon(Data {
70                        timer: tick_attack_or_default(data, self.timer, None),
71                        ..*self
72                    });
73                } else {
74                    // Transitions to recover section of stage
75                    update.character = CharacterState::BasicSummon(Data {
76                        timer: Duration::default(),
77                        stage_section: StageSection::Action,
78                        ..*self
79                    });
80                }
81            },
82            StageSection::Action => {
83                if self.timer < self.static_data.cast_duration
84                    || self.summon_count < self.static_data.summon_amount
85                {
86                    if self.timer
87                        > self.static_data.cast_duration * self.summon_count
88                            / self.static_data.summon_amount
89                    {
90                        let SummonInfo {
91                            body,
92                            loadout_config,
93                            skillset_config,
94                            ..
95                        } = self.static_data.summon_info;
96
97                        let loadout = {
98                            let loadout_builder =
99                                LoadoutBuilder::empty().with_default_maintool(&body);
100                            // If preset is none, use default equipment
101                            if let Some(preset) = loadout_config {
102                                loadout_builder.with_preset(preset).build()
103                            } else {
104                                loadout_builder.with_default_equipment(&body).build()
105                            }
106                        };
107
108                        let skill_set = {
109                            let skillset_builder = SkillSetBuilder::default();
110                            if let Some(preset) = skillset_config {
111                                skillset_builder.with_preset(preset).build()
112                            } else {
113                                skillset_builder.build()
114                            }
115                        };
116
117                        let stats = comp::Stats::new(
118                            self.static_data
119                                .summon_info
120                                .use_npc_name
121                                .then(|| {
122                                    let all_names = NPC_NAMES.read();
123                                    all_names
124                                        .get_species_meta(&self.static_data.summon_info.body)
125                                        .map(|meta| meta.generic.clone())
126                                })
127                                .flatten()
128                                .unwrap_or_else(|| "Summon".to_string()),
129                            body,
130                        );
131
132                        let health = self
133                            .static_data
134                            .summon_info
135                            .has_health
136                            .then(|| comp::Health::new(body));
137
138                        // Ray cast to check where summon should happen
139                        let summon_frac =
140                            self.summon_count as f32 / self.static_data.summon_amount as f32;
141
142                        let length = rand::thread_rng().gen_range(
143                            self.static_data.summon_distance.0..=self.static_data.summon_distance.1,
144                        );
145                        let extra_height =
146                            if self.static_data.summon_info.body == Object(FieryTornado) {
147                                15.0
148                            } else {
149                                0.0
150                            };
151                        let position =
152                            Vec3::new(data.pos.0.x, data.pos.0.y, data.pos.0.z + extra_height);
153                        // Summon in a clockwise fashion
154                        let ray_vector = Vec3::new(
155                            (summon_frac * 2.0 * PI).sin() * length,
156                            (summon_frac * 2.0 * PI).cos() * length,
157                            0.0,
158                        );
159
160                        // Check for collision on the xy plane, subtract 1 to get point before block
161                        let obstacle_xy = data
162                            .terrain
163                            .ray(position, position + length * ray_vector)
164                            .until(Block::is_solid)
165                            .cast()
166                            .0
167                            .sub(1.0);
168
169                        let collision_vector = Vec3::new(
170                            position.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy,
171                            position.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy,
172                            position.z + data.body.eye_height(data.scale.map_or(1.0, |s| s.0)),
173                        );
174
175                        // Check for collision in z up to 50 blocks
176                        let obstacle_z = data
177                            .terrain
178                            .ray(collision_vector, collision_vector - Vec3::unit_z() * 50.0)
179                            .until(Block::is_solid)
180                            .cast()
181                            .0;
182
183                        // If a duration is specified, create a projectile component for the npc
184                        let projectile = self.static_data.duration.map(|duration| Projectile {
185                            hit_solid: Vec::new(),
186                            hit_entity: Vec::new(),
187                            time_left: duration,
188                            owner: Some(*data.uid),
189                            ignore_group: true,
190                            is_sticky: false,
191                            is_point: false,
192                        });
193
194                        let mut rng = rand::thread_rng();
195                        // Send server event to create npc
196                        output_events.emit_server(CreateNpcEvent {
197                            pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
198                            ori: comp::Ori::from(Dir::random_2d(&mut rng)),
199                            npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid))
200                                .with_skill_set(skill_set)
201                                .with_health(health)
202                                .with_inventory(comp::Inventory::with_loadout(loadout, body))
203                                .with_agent(
204                                    comp::Agent::from_body(&body)
205                                        .with_behavior(Behavior::from(BehaviorCapability::SPEAK))
206                                        .with_no_flee_if(true),
207                                )
208                                .with_scale(
209                                    self.static_data
210                                        .summon_info
211                                        .scale
212                                        .unwrap_or(comp::Scale(1.0)),
213                                )
214                                .with_projectile(projectile),
215                            rider: None,
216                        });
217
218                        // Send local event used for frontend shenanigans
219                        output_events.emit_local(LocalEvent::CreateOutcome(
220                            Outcome::SummonedCreature {
221                                pos: data.pos.0,
222                                body,
223                            },
224                        ));
225
226                        update.character = CharacterState::BasicSummon(Data {
227                            timer: tick_attack_or_default(data, self.timer, None),
228                            summon_count: self.summon_count + 1,
229                            ..*self
230                        });
231                    } else {
232                        // Cast
233                        update.character = CharacterState::BasicSummon(Data {
234                            timer: tick_attack_or_default(data, self.timer, None),
235                            ..*self
236                        });
237                    }
238                } else {
239                    // Transitions to recover section of stage
240                    update.character = CharacterState::BasicSummon(Data {
241                        timer: Duration::default(),
242                        stage_section: StageSection::Recover,
243                        ..*self
244                    });
245                }
246            },
247            StageSection::Recover => {
248                if self.timer < self.static_data.recover_duration {
249                    // Recovery
250                    update.character = CharacterState::BasicSummon(Data {
251                        timer: tick_attack_or_default(
252                            data,
253                            self.timer,
254                            Some(data.stats.recovery_speed_modifier),
255                        ),
256                        ..*self
257                    });
258                } else {
259                    // Done
260                    end_ability(data, &mut update);
261                }
262            },
263            _ => {
264                // If it somehow ends up in an incorrect stage section
265                end_ability(data, &mut update);
266            },
267        }
268
269        update
270    }
271}
272
273#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
274pub struct SummonInfo {
275    body: comp::Body,
276    scale: Option<comp::Scale>,
277    has_health: bool,
278    #[serde(default)]
279    use_npc_name: bool,
280    // TODO: use assets for specifying skills and loadout?
281    loadout_config: Option<loadout_builder::Preset>,
282    skillset_config: Option<skillset_builder::Preset>,
283}