veloren_common/states/basic_summon.rs
1use crate::{
2 combat::{AttackTarget, CombatEffect},
3 comp::{
4 self, Behavior, BehaviorCapability, Body, CharacterState, Object, Ori, PidController, Pos,
5 Projectile, StateUpdate, Stats, Vel,
6 ability::Dodgeable,
7 agent, beam,
8 character_state::OutputEvents,
9 inventory::loadout_builder::{self, LoadoutBuilder},
10 object::{self, Body::FieryTornado},
11 },
12 event::{CreateNpcEvent, CreateObjectEvent, LocalEvent, NpcBuilder, SummonBeamPillarsEvent},
13 npc::NPC_NAMES,
14 outcome::Outcome,
15 resources::Secs,
16 skillset_builder::{self, SkillSetBuilder},
17 states::{
18 behavior::{CharacterBehavior, JoinData},
19 utils::*,
20 },
21 terrain::Block,
22 util::Dir,
23 vol::ReadVol,
24};
25use common_i18n::Content;
26use rand::Rng;
27use serde::{Deserialize, Serialize};
28use std::{
29 f32::consts::{PI, TAU},
30 ops::Sub,
31 time::Duration,
32};
33use vek::*;
34
35/// Separated out to condense update portions of character state
36#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
37pub struct StaticData {
38 /// How long the state builds up for
39 pub buildup_duration: Duration,
40 /// How long the state is casting for
41 pub cast_duration: Duration,
42 /// How long the state recovers for
43 pub recover_duration: Duration,
44 /// Information about the summoned entities
45 pub summon_info: SummonInfo,
46 /// Adjusts move speed during the attack per stage
47 pub movement_modifier: MovementModifier,
48 /// Adjusts turning rate during the attack per stage
49 pub ori_modifier: OrientationModifier,
50 /// Miscellaneous information about the ability
51 pub ability_info: AbilityInfo,
52}
53
54#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
55pub struct Data {
56 /// Struct containing data that does not change over the course of the
57 /// character state
58 pub static_data: StaticData,
59 /// How many entities have been summoned
60 pub summon_count: u32,
61 /// Timer for each stage
62 pub timer: Duration,
63 /// What section the character stage is in
64 pub stage_section: StageSection,
65 /// Adjusts move speed during the attack
66 pub movement_modifier: Option<f32>,
67 /// How fast the entity should turn
68 pub ori_modifier: Option<f32>,
69}
70
71impl CharacterBehavior for Data {
72 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
73 let mut update = StateUpdate::from(data);
74
75 let target_uid = || {
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.target_entity)
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::BasicSummon(Data {
88 timer: tick_attack_or_default(data, self.timer, None),
89 ..*self
90 });
91 } else {
92 // Transitions to recover section of stage
93 update.character = CharacterState::BasicSummon(Data {
94 timer: Duration::default(),
95 stage_section: StageSection::Action,
96 movement_modifier: self.static_data.movement_modifier.swing,
97 ori_modifier: self.static_data.ori_modifier.swing,
98 ..*self
99 });
100 }
101 },
102 StageSection::Action => {
103 let summon_amount = self.static_data.summon_info.summon_amount();
104 if self.timer < self.static_data.cast_duration || self.summon_count < summon_amount
105 {
106 if self.timer
107 > self.static_data.cast_duration * self.summon_count / summon_amount
108 {
109 match self.static_data.summon_info {
110 SummonInfo::Npc {
111 summoned_amount: _,
112 summon_distance,
113 body,
114 loadout_config,
115 skillset_config,
116 scale,
117 has_health,
118 use_npc_name,
119 duration,
120 } => {
121 let loadout = {
122 let loadout_builder =
123 LoadoutBuilder::empty().with_default_maintool(&body);
124 // If preset is none, use default equipment
125 if let Some(preset) = loadout_config {
126 loadout_builder.with_preset(preset).build()
127 } else {
128 loadout_builder.with_default_equipment(&body).build()
129 }
130 };
131
132 let skill_set = {
133 let skillset_builder = SkillSetBuilder::default();
134 if let Some(preset) = skillset_config {
135 skillset_builder.with_preset(preset).build()
136 } else {
137 skillset_builder.build()
138 }
139 };
140
141 let stats = comp::Stats::new(
142 use_npc_name
143 .then(|| {
144 let all_names = NPC_NAMES.read();
145 all_names.get_default_name(&body)
146 })
147 .flatten()
148 .unwrap_or_else(|| {
149 Content::with_attr(
150 "npc-custom-fallback-summon",
151 body.gender_attr(),
152 )
153 }),
154 body,
155 );
156
157 let health = has_health.then(|| comp::Health::new(body));
158
159 // Ray cast to check where summon should happen
160 let summon_frac = self.summon_count as f32 / summon_amount as f32;
161
162 let length = rand::thread_rng()
163 .gen_range(summon_distance.0..=summon_distance.1);
164 let extra_height = if body == Body::Object(FieryTornado) {
165 15.0
166 } else {
167 0.0
168 };
169 let position = Vec3::new(
170 data.pos.0.x,
171 data.pos.0.y,
172 data.pos.0.z + extra_height,
173 );
174 // Summon in a clockwise fashion
175 let ray_vector = Vec3::new(
176 (summon_frac * 2.0 * PI).sin() * length,
177 (summon_frac * 2.0 * PI).cos() * length,
178 0.0,
179 );
180
181 // Check for collision on the xy plane, subtract 1 to get point
182 // before block
183 let obstacle_xy = data
184 .terrain
185 .ray(position, position + length * ray_vector)
186 .until(Block::is_solid)
187 .cast()
188 .0
189 .sub(1.0);
190
191 let collision_vector = Vec3::new(
192 position.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy,
193 position.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy,
194 position.z
195 + data.body.eye_height(data.scale.map_or(1.0, |s| s.0)),
196 );
197
198 // Check for collision in z up to 50 blocks
199 let obstacle_z = data
200 .terrain
201 .ray(collision_vector, collision_vector - Vec3::unit_z() * 50.0)
202 .until(Block::is_solid)
203 .cast()
204 .0;
205
206 // If a duration is specified, create a projectile component for the
207 // npc
208 let projectile = duration.map(|duration| Projectile {
209 hit_solid: Vec::new(),
210 hit_entity: Vec::new(),
211 timeout: Vec::new(),
212 time_left: duration,
213 owner: Some(*data.uid),
214 ignore_group: true,
215 is_sticky: false,
216 is_point: false,
217 });
218
219 let mut rng = rand::thread_rng();
220 // Send server event to create npc
221 output_events.emit_server(CreateNpcEvent {
222 pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z),
223 ori: comp::Ori::from(Dir::random_2d(&mut rng)),
224 npc: NpcBuilder::new(
225 stats,
226 body,
227 comp::Alignment::Owned(*data.uid),
228 )
229 .with_skill_set(skill_set)
230 .with_health(health)
231 .with_inventory(comp::Inventory::with_loadout(loadout, body))
232 .with_agent(
233 comp::Agent::from_body(&body)
234 .with_behavior(Behavior::from(
235 BehaviorCapability::SPEAK,
236 ))
237 .with_no_flee_if(true),
238 )
239 .with_scale(scale.unwrap_or(comp::Scale(1.0)))
240 .with_projectile(projectile),
241 });
242
243 // Send local event used for frontend shenanigans
244 output_events.emit_local(LocalEvent::CreateOutcome(
245 Outcome::SummonedCreature {
246 pos: data.pos.0,
247 body,
248 },
249 ));
250 },
251 SummonInfo::BeamPillar {
252 buildup_duration,
253 attack_duration,
254 beam_duration,
255 target,
256 radius,
257 height,
258 damage,
259 damage_effect,
260 dodgeable,
261 tick_rate,
262 specifier,
263 indicator_specifier,
264 } => {
265 let target = match target {
266 BeamPillarTarget::Single => target_uid()
267 .and_then(|target_uid| data.id_maps.uid_entity(target_uid))
268 .map(AttackTarget::Entity),
269 BeamPillarTarget::AllInRange(range) => {
270 Some(AttackTarget::AllInRange(range))
271 },
272 };
273
274 if let Some(target) = target {
275 output_events.emit_server(SummonBeamPillarsEvent {
276 summoner: data.entity,
277 target,
278 buildup_duration: Duration::from_secs_f32(buildup_duration),
279 attack_duration: Duration::from_secs_f32(attack_duration),
280 beam_duration: Duration::from_secs_f32(beam_duration),
281 radius,
282 height,
283 damage,
284 damage_effect,
285 dodgeable,
286 tick_rate,
287 specifier,
288 indicator_specifier,
289 });
290 }
291 },
292 SummonInfo::BeamWall {
293 buildup_duration,
294 attack_duration,
295 beam_duration,
296 pillar_count,
297 wall_radius,
298 pillar_radius,
299 height,
300 damage,
301 damage_effect,
302 dodgeable,
303 tick_rate,
304 specifier,
305 indicator_specifier,
306 } => {
307 let xy_angle = data
308 .ori
309 .to_horizontal()
310 .angle_between(Ori::from(Dir::right()));
311
312 let phi = TAU / pillar_count as f32;
313
314 output_events.emit_server(SummonBeamPillarsEvent {
315 summoner: data.entity,
316 target: AttackTarget::Pos(Vec3::new(
317 data.pos.0.x
318 + (wall_radius
319 * (self.summon_count as f32 * phi + xy_angle)
320 .cos()),
321 data.pos.0.y
322 + (wall_radius
323 * (self.summon_count as f32 * phi + xy_angle)
324 .sin()),
325 data.pos.0.z,
326 )),
327 buildup_duration: Duration::from_secs_f32(buildup_duration),
328 attack_duration: Duration::from_secs_f32(attack_duration),
329 beam_duration: Duration::from_secs_f32(beam_duration),
330 radius: pillar_radius,
331 height,
332 damage,
333 damage_effect,
334 dodgeable,
335 tick_rate,
336 specifier,
337 indicator_specifier,
338 });
339 },
340 SummonInfo::Crux {
341 max_height,
342 scale,
343 range,
344 strength,
345 duration,
346 } => {
347 let body = object::Body::Crux;
348 if let Some((kp, ki, kd)) =
349 agent::pid_coefficients(&Body::Object(body))
350 {
351 let initial_pos = data.pos.0 + 2.0 * Vec3::<f32>::unit_z();
352
353 output_events.emit_server(CreateObjectEvent {
354 pos: Pos(initial_pos),
355 vel: Vel(Vec3::zero()),
356 body: object::Body::Crux,
357 object: Some(Object::Crux {
358 owner: *data.uid,
359 scale,
360 range,
361 strength,
362 duration: Secs(duration),
363 pid_controller: Some(PidController::new(
364 kp,
365 ki,
366 kd,
367 initial_pos.z + max_height,
368 0.0,
369 |sp, pv| sp - pv,
370 )),
371 }),
372 item: None,
373 light_emitter: None,
374 stats: Some(Stats::new(
375 Content::Key(String::from("lantern-crux")),
376 Body::Object(object::Body::Crux),
377 )),
378 });
379 }
380 },
381 }
382
383 update.character = CharacterState::BasicSummon(Data {
384 timer: tick_attack_or_default(data, self.timer, None),
385 summon_count: self.summon_count + 1,
386 ..*self
387 });
388 } else {
389 // Cast
390 update.character = CharacterState::BasicSummon(Data {
391 timer: tick_attack_or_default(data, self.timer, None),
392 ..*self
393 });
394 }
395 } else {
396 // Transitions to recover section of stage
397 update.character = CharacterState::BasicSummon(Data {
398 timer: Duration::default(),
399 stage_section: StageSection::Recover,
400 movement_modifier: self.static_data.movement_modifier.recover,
401 ori_modifier: self.static_data.ori_modifier.recover,
402 ..*self
403 });
404 }
405 },
406 StageSection::Recover => {
407 if self.timer < self.static_data.recover_duration {
408 // Recovery
409 update.character = CharacterState::BasicSummon(Data {
410 timer: tick_attack_or_default(
411 data,
412 self.timer,
413 Some(data.stats.recovery_speed_modifier),
414 ),
415 ..*self
416 });
417 } else {
418 // Done
419 end_ability(data, &mut update);
420 }
421 },
422 _ => {
423 // If it somehow ends up in an incorrect stage section
424 end_ability(data, &mut update);
425 },
426 }
427
428 update
429 }
430}
431
432#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
433pub enum BeamPillarTarget {
434 Single,
435 AllInRange(f32),
436}
437
438#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
439pub enum SummonInfo {
440 Npc {
441 summoned_amount: u32,
442 summon_distance: (f32, f32),
443 body: comp::Body,
444 scale: Option<comp::Scale>,
445 has_health: bool,
446 #[serde(default)]
447 use_npc_name: bool,
448 // TODO: use assets for specifying skills and loadout?
449 loadout_config: Option<loadout_builder::Preset>,
450 skillset_config: Option<skillset_builder::Preset>,
451 duration: Option<Duration>,
452 },
453 BeamPillar {
454 buildup_duration: f32,
455 attack_duration: f32,
456 beam_duration: f32,
457 target: BeamPillarTarget,
458 radius: f32,
459 height: f32,
460 damage: f32,
461 #[serde(default)]
462 damage_effect: Option<CombatEffect>,
463 #[serde(default)]
464 dodgeable: Dodgeable,
465 tick_rate: f32,
466 specifier: beam::FrontendSpecifier,
467 indicator_specifier: BeamPillarIndicatorSpecifier,
468 },
469 BeamWall {
470 buildup_duration: f32,
471 attack_duration: f32,
472 beam_duration: f32,
473 pillar_count: u32,
474 wall_radius: f32,
475 pillar_radius: f32,
476 height: f32,
477 damage: f32,
478 #[serde(default)]
479 damage_effect: Option<CombatEffect>,
480 #[serde(default)]
481 dodgeable: Dodgeable,
482 tick_rate: f32,
483 specifier: beam::FrontendSpecifier,
484 indicator_specifier: BeamPillarIndicatorSpecifier,
485 },
486 Crux {
487 max_height: f32,
488 scale: f32,
489 range: f32,
490 strength: f32,
491 duration: f64,
492 },
493}
494
495impl SummonInfo {
496 fn summon_amount(&self) -> u32 {
497 match self {
498 SummonInfo::Npc {
499 summoned_amount, ..
500 } => *summoned_amount,
501 SummonInfo::BeamPillar { .. } => 1, // Fire pillars are summoned simultaneously
502 SummonInfo::BeamWall { pillar_count, .. } => *pillar_count,
503 SummonInfo::Crux { .. } => 1,
504 }
505 }
506
507 pub fn scale_range(&mut self, scale: f32) {
508 match self {
509 SummonInfo::Npc {
510 summon_distance, ..
511 } => {
512 summon_distance.0 *= scale;
513 summon_distance.1 *= scale;
514 },
515 SummonInfo::BeamPillar { target, .. } => {
516 if let BeamPillarTarget::AllInRange(range) = target {
517 *range *= scale;
518 }
519 },
520 SummonInfo::BeamWall { wall_radius, .. } => {
521 *wall_radius *= scale;
522 },
523 SummonInfo::Crux { .. } => {},
524 }
525 }
526}
527
528#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, strum::EnumString)]
529pub enum BeamPillarIndicatorSpecifier {
530 FirePillar,
531}