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    /// Adjusts turning rate during the attack
50    pub ori_modifier: f32,
51    /// Miscellaneous information about the ability
52    pub ability_info: AbilityInfo,
53}
54
55#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct Data {
57    /// Struct containing data that does not change over the course of the
58    /// character state
59    pub static_data: StaticData,
60    /// Timer for each stage
61    pub timer: Duration,
62    /// What section the character stage is in
63    pub stage_section: StageSection,
64    /// What radius of sprites have already been summoned
65    pub achieved_radius: i32,
66}
67
68impl CharacterBehavior for Data {
69    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
70        let mut update = StateUpdate::from(data);
71
72        handle_orientation(data, &mut update, self.static_data.ori_modifier, None);
73        handle_move(data, &mut update, self.static_data.move_efficiency);
74
75        let target_pos = || {
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.select_pos)
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::SpriteSummon(Data {
88                        timer: tick_attack_or_default(data, self.timer, None),
89                        ..*self
90                    });
91                    // Send local event used for frontend shenanigans
92                    match self.static_data.sprite {
93                        SpriteKind::Empty => output_events.emit_local(LocalEvent::CreateOutcome(
94                            Outcome::TerracottaStatueCharge {
95                                pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
96                            },
97                        )),
98                        SpriteKind::FireBlock => {
99                            output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Charge {
100                                pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
101                            }))
102                        },
103                        _ => {},
104                    }
105                } else {
106                    // Transitions to recover section of stage
107                    update.character = CharacterState::SpriteSummon(Data {
108                        timer: Duration::default(),
109                        stage_section: StageSection::Action,
110                        ..*self
111                    });
112                }
113            },
114            StageSection::Action => {
115                if self.timer < self.static_data.cast_duration {
116                    let timer_frac =
117                        self.timer.as_secs_f32() / self.static_data.cast_duration.as_secs_f32();
118
119                    let anchor_pos = match self.static_data.anchor {
120                        SpriteSummonAnchor::Summoner => data.pos.0,
121                        // Use the selected target position, falling back to the
122                        // summoner position
123                        SpriteSummonAnchor::Target => target_pos().unwrap_or(data.pos.0),
124                    };
125                    let achieved_radius = create_sprites(
126                        data,
127                        output_events,
128                        self.static_data.sprite,
129                        timer_frac,
130                        self.static_data.summon_distance,
131                        self.achieved_radius,
132                        self.static_data.angle,
133                        self.static_data.sparseness,
134                        anchor_pos,
135                        matches!(self.static_data.anchor, SpriteSummonAnchor::Target),
136                        self.static_data.del_timeout,
137                    );
138
139                    update.character = CharacterState::SpriteSummon(Data {
140                        timer: tick_attack_or_default(data, self.timer, None),
141                        achieved_radius,
142                        ..*self
143                    });
144                    // Send local event used for frontend shenanigans
145                    match self.static_data.sprite {
146                        SpriteKind::IceSpike => {
147                            let summoner_pos =
148                                data.pos.0 + *data.ori.look_dir() * data.body.max_radius();
149                            output_events.emit_local(LocalEvent::CreateOutcome(
150                                Outcome::IceCrack {
151                                    pos: match self.static_data.anchor {
152                                        SpriteSummonAnchor::Summoner => summoner_pos,
153                                        SpriteSummonAnchor::Target => {
154                                            target_pos().unwrap_or(summoner_pos)
155                                        },
156                                    },
157                                },
158                            ));
159                        },
160                        SpriteKind::IronSpike => {
161                            output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Whoosh {
162                                pos: data.pos.0,
163                            }));
164                        },
165                        SpriteKind::FireBlock => {
166                            output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Bleep {
167                                pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
168                            }));
169                        },
170                        _ => {},
171                    }
172                } else {
173                    // Transitions to recover section of stage
174                    update.character = CharacterState::SpriteSummon(Data {
175                        timer: Duration::default(),
176                        stage_section: StageSection::Recover,
177                        ..*self
178                    });
179                }
180            },
181            StageSection::Recover => {
182                if self.timer < self.static_data.recover_duration {
183                    // Recovery
184                    update.character = CharacterState::SpriteSummon(Data {
185                        timer: tick_attack_or_default(
186                            data,
187                            self.timer,
188                            Some(data.stats.recovery_speed_modifier),
189                        ),
190                        ..*self
191                    });
192                } else {
193                    // Done
194                    end_ability(data, &mut update);
195                }
196            },
197            _ => {
198                // If it somehow ends up in an incorrect stage section
199                end_ability(data, &mut update);
200            },
201        }
202
203        update
204    }
205}
206
207/// Returns achieved radius
208pub fn create_sprites(
209    data: &JoinData,
210    output_events: &mut OutputEvents,
211    sprite: SpriteKind,
212    timer_frac: f32,
213    summon_distance: (f32, f32),
214    achieved_radius: i32,
215    angle: f32,
216    sparseness: f64,
217    anchor_pos: Vec3<f32>,
218    stack_sprites: bool,
219    del_timeout: Option<(f32, f32)>,
220) -> i32 {
221    // Determines distance from summoner sprites should be created. Goes outward
222    // with time.
223    let summon_distance = timer_frac * (summon_distance.1 - summon_distance.0) + summon_distance.0;
224    let summon_distance = summon_distance.round() as i32;
225
226    // Only summons sprites if summon distance is greater than achieved radius
227    for radius in achieved_radius..=summon_distance {
228        // 1 added to make range correct, too lazy to add 1 to both variables above
229        let radius = radius + 1;
230        // Creates a spiral iterator for the newly achieved radius
231        let spiral = Spiral2d::with_edge_radius(radius);
232        for point in spiral {
233            // If square is in the angle and is not sparse, generate sprite
234            if data
235                .ori
236                .look_vec()
237                .xy()
238                .angle_between(point.as_())
239                .to_degrees()
240                <= (angle / 2.0)
241                && !thread_rng().gen_bool(sparseness)
242            {
243                // The coordinates of where the sprite is created
244                let sprite_pos = Vec3::new(
245                    anchor_pos.x.floor() as i32 + point.x,
246                    anchor_pos.y.floor() as i32 + point.y,
247                    anchor_pos.z.floor() as i32,
248                );
249
250                // Check for collision in z up to 10 blocks up or down
251                let (obstacle_z, obstacle_z_result) = data
252                    .terrain
253                    .ray(
254                        sprite_pos.map(|x| x as f32 + 0.5) + Vec3::unit_z() * 10.0,
255                        sprite_pos.map(|x| x as f32 + 0.5) - Vec3::unit_z() * 10.0,
256                    )
257                    .until(|b| {
258                        // Until reaching a solid block that is not the created
259                        // sprite
260                        Block::is_solid(b) && b.get_sprite() != Some(sprite)
261                    })
262                    .cast();
263
264                let z = match sprite {
265                    // z height - 1 to delete sprite layer below caster
266                    SpriteKind::Empty => sprite_pos.z + (10.5 - obstacle_z).ceil() as i32 - 1,
267                    _ => {
268                        sprite_pos.z
269                            + if let (true, Ok(None)) = (stack_sprites, obstacle_z_result) {
270                                0
271                            } else {
272                                (10.5 - obstacle_z).ceil() as i32
273                            }
274                    },
275                };
276
277                // Location sprite will be created
278                let sprite_pos = Vec3::new(sprite_pos.x, sprite_pos.y, z);
279                // Layers of sprites
280                let layers = match sprite {
281                    SpriteKind::SeaUrchin => 2,
282                    _ => 1,
283                };
284                for i in 0..layers {
285                    // Send server event to create sprite
286                    output_events.emit_server(CreateSpriteEvent {
287                        pos: Vec3::new(sprite_pos.x, sprite_pos.y, z + i),
288                        sprite,
289                        del_timeout,
290                    });
291                }
292            }
293        }
294    }
295    summon_distance
296}