Skip to main content

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::{RngExt, 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(data, self.timer, None),
186                        ..*self
187                    });
188                } else {
189                    // Done
190                    end_ability(data, &mut update);
191                }
192            },
193            _ => {
194                // If it somehow ends up in an incorrect stage section
195                end_ability(data, &mut update);
196            },
197        }
198
199        update
200    }
201}
202
203/// Returns achieved radius
204pub fn create_sprites(
205    data: &JoinData,
206    output_events: &mut OutputEvents,
207    sprite: SpriteKind,
208    timer_frac: f32,
209    summon_distance: (f32, f32),
210    achieved_radius: i32,
211    angle: f32,
212    sparseness: f64,
213    anchor_pos: Vec3<f32>,
214    stack_sprites: bool,
215    del_timeout: Option<(f32, f32)>,
216) -> i32 {
217    // Determines distance from summoner sprites should be created. Goes outward
218    // with time.
219    let summon_distance = timer_frac * (summon_distance.1 - summon_distance.0) + summon_distance.0;
220    let summon_distance = summon_distance.round() as i32;
221
222    // Only summons sprites if summon distance is greater than achieved radius
223    for radius in achieved_radius..=summon_distance {
224        // 1 added to make range correct, too lazy to add 1 to both variables above
225        let radius = radius + 1;
226        // Creates a spiral iterator for the newly achieved radius
227        let spiral = Spiral2d::with_edge_radius(radius);
228        for point in spiral {
229            // If square is in the angle and is not sparse, generate sprite
230            if data
231                .ori
232                .look_vec()
233                .xy()
234                .angle_between(point.as_())
235                .to_degrees()
236                <= (angle / 2.0)
237                && !rng().random_bool(sparseness)
238            {
239                // The coordinates of where the sprite is created
240                let sprite_pos = Vec3::new(
241                    anchor_pos.x.floor() as i32 + point.x,
242                    anchor_pos.y.floor() as i32 + point.y,
243                    anchor_pos.z.floor() as i32,
244                );
245
246                // Check for collision in z up to 10 blocks up or down
247                let (obstacle_z, obstacle_z_result) = data
248                    .terrain
249                    .ray(
250                        sprite_pos.map(|x| x as f32 + 0.5) + Vec3::unit_z() * 10.0,
251                        sprite_pos.map(|x| x as f32 + 0.5) - Vec3::unit_z() * 10.0,
252                    )
253                    .until(|b| {
254                        // Until reaching a solid block that is not the created
255                        // sprite
256                        Block::is_solid(b) && b.get_sprite() != Some(sprite)
257                    })
258                    .cast();
259
260                let z = match sprite {
261                    // z height - 1 to delete sprite layer below caster
262                    SpriteKind::Empty => sprite_pos.z + (10.5 - obstacle_z).ceil() as i32 - 1,
263                    _ => {
264                        sprite_pos.z
265                            + if let (true, Ok(None)) = (stack_sprites, obstacle_z_result) {
266                                0
267                            } else {
268                                (10.5 - obstacle_z).ceil() as i32
269                            }
270                    },
271                };
272
273                // Location sprite will be created
274                let sprite_pos = Vec3::new(sprite_pos.x, sprite_pos.y, z);
275                // Layers of sprites
276                let layers = match sprite {
277                    SpriteKind::SeaUrchin => 2,
278                    _ => 1,
279                };
280                for i in 0..layers {
281                    // Send server event to create sprite
282                    output_events.emit_server(CreateSpriteEvent {
283                        pos: Vec3::new(sprite_pos.x, sprite_pos.y, z + i),
284                        sprite,
285                        del_timeout,
286                    });
287                }
288            }
289        }
290    }
291    summon_distance
292}