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