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