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#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
27pub struct StaticData {
28 pub buildup_duration: Duration,
30 pub cast_duration: Duration,
32 pub recover_duration: Duration,
34 pub sprite: SpriteKind,
36 pub del_timeout: Option<(f32, f32)>,
39 pub summon_distance: (f32, f32),
41 pub anchor: SpriteSummonAnchor,
43 pub sparseness: f64,
45 pub angle: f32,
47 pub move_efficiency: f32,
49 pub ability_info: AbilityInfo,
51}
52
53#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
54pub struct Data {
55 pub static_data: StaticData,
58 pub timer: Duration,
60 pub stage_section: StageSection,
62 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 update.character = CharacterState::SpriteSummon(Data {
86 timer: tick_attack_or_default(data, self.timer, None),
87 ..*self
88 });
89 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 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 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 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 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 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 end_ability(data, &mut update);
196 }
197 },
198 _ => {
199 end_ability(data, &mut update);
201 },
202 }
203
204 update
205 }
206}
207
208pub 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 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 for radius in achieved_radius..=summon_distance {
229 let radius = radius + 1;
231 let spiral = Spiral2d::with_edge_radius(radius);
233 for point in spiral {
234 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 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 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 Block::is_solid(b) && b.get_sprite() != Some(sprite)
262 })
263 .cast();
264
265 let z = match sprite {
266 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 let sprite_pos = Vec3::new(sprite_pos.x, sprite_pos.y, z);
280 let layers = match sprite {
282 SpriteKind::SeaUrchin => 2,
283 _ => 1,
284 };
285 for i in 0..layers {
286 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}