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