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 rand::Rng;
23use serde::{Deserialize, Serialize};
24use std::{f32::consts::PI, ops::Sub, time::Duration};
25use vek::*;
26
27#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct StaticData {
30 pub buildup_duration: Duration,
32 pub cast_duration: Duration,
34 pub recover_duration: Duration,
36 pub summon_amount: u32,
38 pub summon_distance: (f32, f32),
40 pub summon_info: SummonInfo,
42 pub ability_info: AbilityInfo,
44 pub duration: Option<Duration>,
46}
47
48#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct Data {
50 pub static_data: StaticData,
53 pub summon_count: u32,
55 pub timer: Duration,
57 pub stage_section: StageSection,
59}
60
61impl CharacterBehavior for Data {
62 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
63 let mut update = StateUpdate::from(data);
64
65 match self.stage_section {
66 StageSection::Buildup => {
67 if self.timer < self.static_data.buildup_duration {
68 update.character = CharacterState::BasicSummon(Data {
70 timer: tick_attack_or_default(data, self.timer, None),
71 ..*self
72 });
73 } else {
74 update.character = CharacterState::BasicSummon(Data {
76 timer: Duration::default(),
77 stage_section: StageSection::Action,
78 ..*self
79 });
80 }
81 },
82 StageSection::Action => {
83 if self.timer < self.static_data.cast_duration
84 || self.summon_count < self.static_data.summon_amount
85 {
86 if self.timer
87 > self.static_data.cast_duration * self.summon_count
88 / self.static_data.summon_amount
89 {
90 let SummonInfo {
91 body,
92 loadout_config,
93 skillset_config,
94 ..
95 } = self.static_data.summon_info;
96
97 let loadout = {
98 let loadout_builder =
99 LoadoutBuilder::empty().with_default_maintool(&body);
100 if let Some(preset) = loadout_config {
102 loadout_builder.with_preset(preset).build()
103 } else {
104 loadout_builder.with_default_equipment(&body).build()
105 }
106 };
107
108 let skill_set = {
109 let skillset_builder = SkillSetBuilder::default();
110 if let Some(preset) = skillset_config {
111 skillset_builder.with_preset(preset).build()
112 } else {
113 skillset_builder.build()
114 }
115 };
116
117 let stats = comp::Stats::new(
118 self.static_data
119 .summon_info
120 .use_npc_name
121 .then(|| {
122 let all_names = NPC_NAMES.read();
123 all_names
124 .get_species_meta(&self.static_data.summon_info.body)
125 .map(|meta| meta.generic.clone())
126 })
127 .flatten()
128 .unwrap_or_else(|| "Summon".to_string()),
129 body,
130 );
131
132 let health = self
133 .static_data
134 .summon_info
135 .has_health
136 .then(|| comp::Health::new(body));
137
138 let summon_frac =
140 self.summon_count as f32 / self.static_data.summon_amount as f32;
141
142 let length = rand::thread_rng().gen_range(
143 self.static_data.summon_distance.0..=self.static_data.summon_distance.1,
144 );
145 let extra_height =
146 if self.static_data.summon_info.body == Object(FieryTornado) {
147 15.0
148 } else {
149 0.0
150 };
151 let position =
152 Vec3::new(data.pos.0.x, data.pos.0.y, data.pos.0.z + extra_height);
153 let ray_vector = Vec3::new(
155 (summon_frac * 2.0 * PI).sin() * length,
156 (summon_frac * 2.0 * PI).cos() * length,
157 0.0,
158 );
159
160 let obstacle_xy = data
162 .terrain
163 .ray(position, position + length * ray_vector)
164 .until(Block::is_solid)
165 .cast()
166 .0
167 .sub(1.0);
168
169 let collision_vector = Vec3::new(
170 position.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy,
171 position.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy,
172 position.z + data.body.eye_height(data.scale.map_or(1.0, |s| s.0)),
173 );
174
175 let obstacle_z = data
177 .terrain
178 .ray(collision_vector, collision_vector - Vec3::unit_z() * 50.0)
179 .until(Block::is_solid)
180 .cast()
181 .0;
182
183 let projectile = self.static_data.duration.map(|duration| Projectile {
185 hit_solid: Vec::new(),
186 hit_entity: Vec::new(),
187 time_left: duration,
188 owner: Some(*data.uid),
189 ignore_group: true,
190 is_sticky: false,
191 is_point: false,
192 });
193
194 let mut rng = rand::thread_rng();
195 output_events.emit_server(CreateNpcEvent {
197 pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
198 ori: comp::Ori::from(Dir::random_2d(&mut rng)),
199 npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid))
200 .with_skill_set(skill_set)
201 .with_health(health)
202 .with_inventory(comp::Inventory::with_loadout(loadout, body))
203 .with_agent(
204 comp::Agent::from_body(&body)
205 .with_behavior(Behavior::from(BehaviorCapability::SPEAK))
206 .with_no_flee_if(true),
207 )
208 .with_scale(
209 self.static_data
210 .summon_info
211 .scale
212 .unwrap_or(comp::Scale(1.0)),
213 )
214 .with_projectile(projectile),
215 rider: None,
216 });
217
218 output_events.emit_local(LocalEvent::CreateOutcome(
220 Outcome::SummonedCreature {
221 pos: data.pos.0,
222 body,
223 },
224 ));
225
226 update.character = CharacterState::BasicSummon(Data {
227 timer: tick_attack_or_default(data, self.timer, None),
228 summon_count: self.summon_count + 1,
229 ..*self
230 });
231 } else {
232 update.character = CharacterState::BasicSummon(Data {
234 timer: tick_attack_or_default(data, self.timer, None),
235 ..*self
236 });
237 }
238 } else {
239 update.character = CharacterState::BasicSummon(Data {
241 timer: Duration::default(),
242 stage_section: StageSection::Recover,
243 ..*self
244 });
245 }
246 },
247 StageSection::Recover => {
248 if self.timer < self.static_data.recover_duration {
249 update.character = CharacterState::BasicSummon(Data {
251 timer: tick_attack_or_default(
252 data,
253 self.timer,
254 Some(data.stats.recovery_speed_modifier),
255 ),
256 ..*self
257 });
258 } else {
259 end_ability(data, &mut update);
261 }
262 },
263 _ => {
264 end_ability(data, &mut update);
266 },
267 }
268
269 update
270 }
271}
272
273#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
274pub struct SummonInfo {
275 body: comp::Body,
276 scale: Option<comp::Scale>,
277 has_health: bool,
278 #[serde(default)]
279 use_npc_name: bool,
280 loadout_config: Option<loadout_builder::Preset>,
282 skillset_config: Option<skillset_builder::Preset>,
283}