veloren_common/states/
sprite_summon.rs

1use crate::{
2    comp::{CharacterState, StateUpdate, character_state::OutputEvents},
3    event::{CreateSpriteEvent, LocalEvent},
4    outcome::Outcome,
5    spiral::Spiral2d,
6    states::{
7        behavior::{CharacterBehavior, JoinData},
8        utils::*,
9    },
10    terrain::{Block, SpriteKind},
11    vol::ReadVol,
12};
13use rand::{Rng, thread_rng};
14use serde::{Deserialize, Serialize};
15use std::time::Duration;
16use vek::*;
17
18#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
19pub enum SpriteSummonAnchor {
20    #[default]
21    Summoner,
22    Target,
23}
24
25/// Separated out to condense update portions of character state
26#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
27pub struct StaticData {
28    /// How long the state builds up for
29    pub buildup_duration: Duration,
30    /// How long the state is casting for
31    pub cast_duration: Duration,
32    /// How long the state recovers for
33    pub recover_duration: Duration,
34    /// What kind of sprite is created by this state
35    pub sprite: SpriteKind,
36    /// Duration until sprite-delete begins (in sec), randomization-range of
37    /// sprite-delete-time (in sec)
38    pub del_timeout: Option<(f32, f32)>,
39    /// Range that sprites are created relative to the summonner
40    pub summon_distance: (f32, f32),
41    /// Relative to what should the sprites be summoned?
42    pub anchor: SpriteSummonAnchor,
43    /// Chance that sprite is not created on a particular square
44    pub sparseness: f64,
45    /// Angle of total coverage, centered on the forward-facing orientation
46    pub angle: f32,
47    /// How much we can move
48    pub move_efficiency: f32,
49    /// Miscellaneous information about the ability
50    pub ability_info: AbilityInfo,
51}
52
53#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
54pub struct Data {
55    /// Struct containing data that does not change over the course of the
56    /// character state
57    pub static_data: StaticData,
58    /// Timer for each stage
59    pub timer: Duration,
60    /// What section the character stage is in
61    pub stage_section: StageSection,
62    /// What radius of sprites have already been summoned
63    pub achieved_radius: i32,
64}
65
66impl CharacterBehavior for Data {
67    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
68        let mut update = StateUpdate::from(data);
69
70        handle_orientation(data, &mut update, 1.0, None);
71        handle_move(data, &mut update, self.static_data.move_efficiency);
72
73        let target_pos = || {
74            data.controller
75                .queued_inputs
76                .get(&self.static_data.ability_info.input)
77                .or(self.static_data.ability_info.input_attr.as_ref())
78                .and_then(|input| input.select_pos)
79        };
80
81        match self.stage_section {
82            StageSection::Buildup => {
83                if self.timer < self.static_data.buildup_duration {
84                    // Build up
85                    update.character = CharacterState::SpriteSummon(Data {
86                        timer: tick_attack_or_default(data, self.timer, None),
87                        ..*self
88                    });
89                    // Send local event used for frontend shenanigans
90                    match self.static_data.sprite {
91                        SpriteKind::Empty => {
92                            output_events.emit_local(LocalEvent::CreateOutcome(
93                                Outcome::TerracottaStatueCharge {
94                                    pos: data.pos.0
95                                        + *data.ori.look_dir() * (data.body.max_radius()),
96                                },
97                            ));
98                        },
99                        SpriteKind::FireBlock => {
100                            output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Charge {
101                                pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
102                            }));
103                        },
104                        _ => {},
105                    }
106                } else {
107                    // Transitions to recover section of stage
108                    update.character = CharacterState::SpriteSummon(Data {
109                        timer: Duration::default(),
110                        stage_section: StageSection::Action,
111                        ..*self
112                    });
113                }
114            },
115            StageSection::Action => {
116                if self.timer < self.static_data.cast_duration {
117                    let timer_frac =
118                        self.timer.as_secs_f32() / self.static_data.cast_duration.as_secs_f32();
119
120                    let anchor_pos = match self.static_data.anchor {
121                        SpriteSummonAnchor::Summoner => data.pos.0,
122                        // Use the selected target position, falling back to the
123                        // summoner position
124                        SpriteSummonAnchor::Target => target_pos().unwrap_or(data.pos.0),
125                    };
126                    let achieved_radius = create_sprites(
127                        data,
128                        output_events,
129                        self.static_data.sprite,
130                        timer_frac,
131                        self.static_data.summon_distance,
132                        self.achieved_radius,
133                        self.static_data.angle,
134                        self.static_data.sparseness,
135                        anchor_pos,
136                        matches!(self.static_data.anchor, SpriteSummonAnchor::Target),
137                        self.static_data.del_timeout,
138                    );
139
140                    update.character = CharacterState::SpriteSummon(Data {
141                        timer: tick_attack_or_default(data, self.timer, None),
142                        achieved_radius,
143                        ..*self
144                    });
145                    // Send local event used for frontend shenanigans
146                    match self.static_data.sprite {
147                        SpriteKind::IceSpike => {
148                            let summoner_pos =
149                                data.pos.0 + *data.ori.look_dir() * data.body.max_radius();
150                            output_events.emit_local(LocalEvent::CreateOutcome(
151                                Outcome::IceCrack {
152                                    pos: match self.static_data.anchor {
153                                        SpriteSummonAnchor::Summoner => summoner_pos,
154                                        SpriteSummonAnchor::Target => {
155                                            target_pos().unwrap_or(summoner_pos)
156                                        },
157                                    },
158                                },
159                            ));
160                        },
161                        SpriteKind::IronSpike => {
162                            output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Whoosh {
163                                pos: data.pos.0,
164                            }));
165                        },
166                        SpriteKind::FireBlock => {
167                            output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Bleep {
168                                pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
169                            }));
170                        },
171                        _ => {},
172                    }
173                } else {
174                    // Transitions to recover section of stage
175                    update.character = CharacterState::SpriteSummon(Data {
176                        timer: Duration::default(),
177                        stage_section: StageSection::Recover,
178                        ..*self
179                    });
180                }
181            },
182            StageSection::Recover => {
183                if self.timer < self.static_data.recover_duration {
184                    // Recovery
185                    update.character = CharacterState::SpriteSummon(Data {
186                        timer: tick_attack_or_default(
187                            data,
188                            self.timer,
189                            Some(data.stats.recovery_speed_modifier),
190                        ),
191                        ..*self
192                    });
193                } else {
194                    // Done
195                    end_ability(data, &mut update);
196                }
197            },
198            _ => {
199                // If it somehow ends up in an incorrect stage section
200                end_ability(data, &mut update);
201            },
202        }
203
204        update
205    }
206}
207
208/// Returns achieved radius
209pub fn create_sprites(
210    data: &JoinData,
211    output_events: &mut OutputEvents,
212    sprite: SpriteKind,
213    timer_frac: f32,
214    summon_distance: (f32, f32),
215    achieved_radius: i32,
216    angle: f32,
217    sparseness: f64,
218    anchor_pos: Vec3<f32>,
219    stack_sprites: bool,
220    del_timeout: Option<(f32, f32)>,
221) -> i32 {
222    // Determines distance from summoner sprites should be created. Goes outward
223    // with time.
224    let summon_distance = timer_frac * (summon_distance.1 - summon_distance.0) + summon_distance.0;
225    let summon_distance = summon_distance.round() as i32;
226
227    // Only summons sprites if summon distance is greater than achieved radius
228    for radius in achieved_radius..=summon_distance {
229        // 1 added to make range correct, too lazy to add 1 to both variables above
230        let radius = radius + 1;
231        // Creates a spiral iterator for the newly achieved radius
232        let spiral = Spiral2d::with_edge_radius(radius);
233        for point in spiral {
234            // If square is in the angle and is not sparse, generate sprite
235            if data
236                .ori
237                .look_vec()
238                .xy()
239                .angle_between(point.as_())
240                .to_degrees()
241                <= (angle / 2.0)
242                && !thread_rng().gen_bool(sparseness)
243            {
244                // The coordinates of where the sprite is created
245                let sprite_pos = Vec3::new(
246                    anchor_pos.x.floor() as i32 + point.x,
247                    anchor_pos.y.floor() as i32 + point.y,
248                    anchor_pos.z.floor() as i32,
249                );
250
251                // Check for collision in z up to 10 blocks up or down
252                let (obstacle_z, obstacle_z_result) = data
253                    .terrain
254                    .ray(
255                        sprite_pos.map(|x| x as f32 + 0.5) + Vec3::unit_z() * 10.0,
256                        sprite_pos.map(|x| x as f32 + 0.5) - Vec3::unit_z() * 10.0,
257                    )
258                    .until(|b| {
259                        // Until reaching a solid block that is not the created
260                        // sprite
261                        Block::is_solid(b) && b.get_sprite() != Some(sprite)
262                    })
263                    .cast();
264
265                let z = match sprite {
266                    // z height - 1 to delete sprite layer below caster
267                    SpriteKind::Empty => sprite_pos.z + (10.5 - obstacle_z).ceil() as i32 - 1,
268                    _ => {
269                        sprite_pos.z
270                            + if let (true, Ok(None)) = (stack_sprites, obstacle_z_result) {
271                                0
272                            } else {
273                                (10.5 - obstacle_z).ceil() as i32
274                            }
275                    },
276                };
277
278                // Location sprite will be created
279                let sprite_pos = Vec3::new(sprite_pos.x, sprite_pos.y, z);
280                // Layers of sprites
281                let layers = match sprite {
282                    SpriteKind::SeaUrchin => 2,
283                    _ => 1,
284                };
285                for i in 0..layers {
286                    // Send server event to create sprite
287                    output_events.emit_server(CreateSpriteEvent {
288                        pos: Vec3::new(sprite_pos.x, sprite_pos.y, z + i),
289                        sprite,
290                        del_timeout,
291                    });
292                }
293            }
294        }
295    }
296    summon_distance
297}