1use crate::{
2 consts::MAX_PATH_DIST,
3 data::*,
4 util::{entities_have_line_of_sight, handle_attack_aggression},
5};
6use common::{
7 combat::{self, AttackSource},
8 comp::{
9 Ability, AbilityInput, Agent, CharacterAbility, CharacterState, ControlAction,
10 ControlEvent, Controller, Fluid, InputKind,
11 ability::{ActiveAbilities, AuxiliaryAbility, BASE_ABILITY_LIMIT, Stance, SwordStance},
12 buff::BuffKind,
13 item::tool::AbilityContext,
14 skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
15 },
16 consts::GRAVITY,
17 path::TraversalConfig,
18 states::{
19 self_buff,
20 sprite_summon::{self, SpriteSummonAnchor},
21 utils::StageSection,
22 },
23 terrain::Block,
24 util::Dir,
25 vol::ReadVol,
26};
27use rand::{Rng, prelude::SliceRandom};
28use std::{f32::consts::PI, time::Duration};
29use vek::*;
30use world::util::CARDINALS;
31
32fn projectile_flat_range(speed: f32, height: f32) -> f32 {
34 let w = speed.powi(2);
35 let u = 0.5 * 2_f32.sqrt() * speed;
36 (0.5 * w + u * (0.5 * w + 2.0 * GRAVITY * height).sqrt()) / GRAVITY
37}
38
39fn projectile_multi_angle(projectile_spread: f32, num_projectiles: u32) -> f32 {
41 (180.0 / PI) * projectile_spread * (num_projectiles - 1) as f32
42}
43
44fn rng_from_span(rng: &mut impl Rng, span: [f32; 2]) -> f32 { rng.gen_range(span[0]..=span[1]) }
45
46impl AgentData<'_> {
47 pub fn handle_simple_melee(
50 &self,
51 agent: &mut Agent,
52 controller: &mut Controller,
53 attack_data: &AttackData,
54 tgt_data: &TargetData,
55 read_data: &ReadData,
56 rng: &mut impl Rng,
57 ) {
58 if attack_data.in_min_range() && attack_data.angle < 30.0 {
59 controller.push_basic_input(InputKind::Primary);
60 controller.inputs.move_dir = Vec2::zero();
61 } else {
62 self.path_toward_target(
63 agent,
64 controller,
65 tgt_data.pos.0,
66 read_data,
67 Path::Full,
68 None,
69 );
70 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
71 && attack_data.dist_sqrd < 16.0f32.powi(2)
72 && rng.gen::<f32>() < 0.02
73 {
74 controller.push_basic_input(InputKind::Roll);
75 }
76 }
77 }
78
79 pub fn handle_simple_flying_melee(
82 &self,
83 _agent: &mut Agent,
84 controller: &mut Controller,
85 attack_data: &AttackData,
86 tgt_data: &TargetData,
87 read_data: &ReadData,
88 _rng: &mut impl Rng,
89 ) {
90 let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
92 .try_normalized()
93 .unwrap_or_else(Vec3::zero);
94 let speed = 1.0;
95 controller.inputs.move_dir = dir_to_target.xy() * speed;
96
97 controller.push_basic_input(InputKind::Fly);
99 if self.physics_state.on_ground.is_some() {
103 controller.push_basic_input(InputKind::Jump);
104 } else {
105 let mut maintain_altitude = |set_point| {
108 let alt = read_data
109 .terrain
110 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
111 .until(Block::is_solid)
112 .cast()
113 .0;
114 let error = set_point - alt;
115 controller.inputs.move_z = error;
116 };
117 if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
118 maintain_altitude(5.0);
119 } else {
120 maintain_altitude(2.0);
121
122 if attack_data.dist_sqrd < 3.5_f32.powi(2) && attack_data.angle < 150.0 {
124 controller.push_basic_input(InputKind::Primary);
125 }
126 }
127 }
128 }
129
130 pub fn handle_bloodmoon_bat_attack(
131 &self,
132 agent: &mut Agent,
133 controller: &mut Controller,
134 attack_data: &AttackData,
135 tgt_data: &TargetData,
136 read_data: &ReadData,
137 _rng: &mut impl Rng,
138 ) {
139 enum ActionStateTimers {
140 AttackTimer,
141 }
142
143 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
144
145 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
146 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
147 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
149 }
150
151 let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
152 .try_normalized()
153 .unwrap_or_else(Vec3::zero);
154 let speed = 1.0;
155 controller.inputs.move_dir = dir_to_target.xy() * speed;
156
157 controller.push_basic_input(InputKind::Fly);
159 if self.physics_state.on_ground.is_some() {
160 controller.push_basic_input(InputKind::Jump);
161 } else {
162 let mut maintain_altitude = |set_point| {
165 let alt = read_data
166 .terrain
167 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
168 .until(Block::is_solid)
169 .cast()
170 .0;
171 let error = set_point - alt;
172 controller.inputs.move_z = error;
173 };
174 if !(-20.6..20.6).contains(&(tgt_data.pos.0.y - home.y))
175 || !(-26.6..26.6).contains(&(tgt_data.pos.0.x - home.x))
176 {
177 if (home - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
178 controller.push_action(ControlAction::StartInput {
179 input: InputKind::Ability(0),
180 target_entity: None,
181 select_pos: Some(home),
182 });
183 } else {
184 controller.push_basic_input(InputKind::Ability(1));
185 }
186 } else if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
187 maintain_altitude(5.0);
188 } else {
189 maintain_altitude(2.0);
190 if tgt_data.pos.0.z < home.z + 5.0 && self.pos.0.z < home.z + 25.0 {
191 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
192 controller.push_basic_input(InputKind::Secondary);
193 } else {
194 controller.push_basic_input(InputKind::Ability(1));
195 }
196 } else if attack_data.dist_sqrd < 6.0_f32.powi(2) {
197 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 2.0 {
199 controller.push_basic_input(InputKind::Ability(2));
200 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize]
201 < 4.0
202 {
203 controller.push_basic_input(InputKind::Ability(3));
204 } else {
205 controller.push_basic_input(InputKind::Primary);
206 }
207 } else if tgt_data.pos.0.z < home.z + 30.0
208 && agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0
209 {
210 controller.push_action(ControlAction::StartInput {
211 input: InputKind::Ability(0),
212 target_entity: agent
213 .target
214 .as_ref()
215 .and_then(|t| read_data.uids.get(t.target))
216 .copied(),
217 select_pos: None,
218 });
219 }
220 }
221 }
222 }
223
224 pub fn handle_vampire_bat_attack(
225 &self,
226 agent: &mut Agent,
227 controller: &mut Controller,
228 _attack_data: &AttackData,
229 _tgt_data: &TargetData,
230 read_data: &ReadData,
231 _rng: &mut impl Rng,
232 ) {
233 enum ActionStateTimers {
234 AttackTimer,
235 }
236
237 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
238 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 9.0 {
239 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
241 }
242
243 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
245 self.path_toward_target(agent, controller, home, read_data, Path::Full, None);
246 if (home - self.pos.0).xy().magnitude_squared() > (10.0_f32).powi(2) {
248 controller.push_action(ControlAction::StartInput {
249 input: InputKind::Ability(1),
250 target_entity: None,
251 select_pos: Some(home),
252 });
253 }
254 controller.push_basic_input(InputKind::Fly);
256 if self.pos.0.z < home.z + 4.0
257 && agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 6.0
258 {
259 controller.push_basic_input(InputKind::Secondary);
260 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0
261 && (self.pos.0.z - home.z) < 110.0
262 {
263 controller.push_basic_input(InputKind::Primary);
264 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 6.0 {
265 controller.push_basic_input(InputKind::Ability(0));
266 }
267 }
268
269 pub fn handle_bloodmoon_heiress_attack(
270 &self,
271 agent: &mut Agent,
272 controller: &mut Controller,
273 attack_data: &AttackData,
274 tgt_data: &TargetData,
275 read_data: &ReadData,
276 rng: &mut impl Rng,
277 ) {
278 const DASH_TIMER: usize = 0;
279 const SUMMON_THRESHOLD: f32 = 0.20;
280 enum ActionStateFCounters {
281 FCounterHealthThreshold = 0,
282 }
283 enum ActionStateConditions {
284 ConditionCounterInit = 0,
285 }
286 agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
287 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
288 let line_of_sight_with_target = || {
289 entities_have_line_of_sight(
290 self.pos,
291 self.body,
292 self.scale,
293 tgt_data.pos,
294 tgt_data.body,
295 tgt_data.scale,
296 read_data,
297 )
298 };
299 if !agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] {
302 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
303 1.0 - SUMMON_THRESHOLD;
304 agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] =
305 true;
306 }
307
308 if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
309 > health_fraction
310 {
311 controller.push_basic_input(InputKind::Ability(2));
313
314 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
315 {
316 agent.combat_state.counters
317 [ActionStateFCounters::FCounterHealthThreshold as usize] -= SUMMON_THRESHOLD;
318 }
319 }
320 else if self
322 .path_toward_target(
323 agent,
324 controller,
325 tgt_data.pos.0,
326 read_data,
327 Path::Separate,
328 None,
329 )
330 .is_none()
331 || !(-3.0..3.0).contains(&(tgt_data.pos.0.z - self.pos.0.z))
332 {
333 controller.push_action(ControlAction::StartInput {
334 input: InputKind::Ability(0),
335 target_entity: agent
336 .target
337 .as_ref()
338 .and_then(|t| read_data.uids.get(t.target))
339 .copied(),
340 select_pos: None,
341 });
342 } else if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
343 {
344 controller.push_basic_input(InputKind::Secondary);
345 } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
346 if agent.combat_state.timers[DASH_TIMER] > 2.0 {
347 agent.combat_state.timers[DASH_TIMER] = 0.0;
348 }
349 match rng.gen_range(0..2) {
350 0 => controller.push_basic_input(InputKind::Primary),
351 _ => controller.push_basic_input(InputKind::Ability(3)),
352 };
353 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
354 && self
355 .path_toward_target(
356 agent,
357 controller,
358 tgt_data.pos.0,
359 read_data,
360 Path::Separate,
361 None,
362 )
363 .is_some()
364 && line_of_sight_with_target()
365 && agent.combat_state.timers[DASH_TIMER] > 4.0
366 && attack_data.angle < 45.0
367 {
368 match rng.gen_range(0..2) {
369 0 => controller.push_basic_input(InputKind::Secondary),
370 _ => controller.push_basic_input(InputKind::Ability(1)),
371 };
372 agent.combat_state.timers[DASH_TIMER] = 0.0;
373 } else {
374 self.path_toward_target(
375 agent,
376 controller,
377 tgt_data.pos.0,
378 read_data,
379 Path::Partial,
380 None,
381 );
382 }
383 }
384
385 pub fn handle_simple_backstab(
389 &self,
390 agent: &mut Agent,
391 controller: &mut Controller,
392 attack_data: &AttackData,
393 tgt_data: &TargetData,
394 read_data: &ReadData,
395 ) {
396 const STRAFE_DIST: f32 = 4.5;
398 const STRAFE_SPEED_MULT: f32 = 0.75;
399 const STRAFE_SPIRAL_MULT: f32 = 0.8; const BACKSTAB_SPEED_MULT: f32 = 0.3;
401
402 let target_ori = agent
404 .target
405 .and_then(|t| read_data.orientations.get(t.target))
406 .map(|ori| ori.look_vec())
407 .unwrap_or_default();
408 let dist = attack_data.dist_sqrd.sqrt();
409 let in_front_of_target = target_ori.dot(self.pos.0 - tgt_data.pos.0) > 0.0;
410
411 if attack_data.in_min_range() && attack_data.angle < 30.0 {
413 controller.push_basic_input(InputKind::Primary);
414 controller.inputs.move_dir = Vec2::zero();
415 }
416
417 if attack_data.dist_sqrd < STRAFE_DIST.powi(2) {
418 let vec_to_target = (tgt_data.pos.0 - self.pos.0).xy();
421 if in_front_of_target {
422 let theta = (PI / 2. - dist * 0.1).max(0.0);
423 let potential_move_dirs = [
425 vec_to_target
426 .rotated_z(theta)
427 .try_normalized()
428 .unwrap_or_default(),
429 vec_to_target
430 .rotated_z(-theta)
431 .try_normalized()
432 .unwrap_or_default(),
433 ];
434 if let Some(move_dir) = potential_move_dirs
436 .iter()
437 .find(|move_dir| target_ori.xy().dot(**move_dir) < 0.0)
438 {
439 controller.inputs.move_dir =
440 STRAFE_SPEED_MULT * (*move_dir - STRAFE_SPIRAL_MULT * target_ori.xy());
441 }
442 } else {
443 let move_target = tgt_data.pos.0.xy() - dist / 2. * target_ori.xy();
446 controller.inputs.move_dir = ((move_target - self.pos.0) * BACKSTAB_SPEED_MULT)
447 .try_normalized()
448 .unwrap_or_default();
449 }
450 } else {
451 self.path_toward_target(
452 agent,
453 controller,
454 tgt_data.pos.0,
455 read_data,
456 Path::Full,
457 None,
458 );
459 }
460 }
461
462 pub fn handle_elevated_ranged(
463 &self,
464 agent: &mut Agent,
465 controller: &mut Controller,
466 attack_data: &AttackData,
467 tgt_data: &TargetData,
468 read_data: &ReadData,
469 ) {
470 const PREF_DIST: f32 = 30.0;
472 const RETREAT_DIST: f32 = 8.0;
473
474 let line_of_sight_with_target = || {
475 entities_have_line_of_sight(
476 self.pos,
477 self.body,
478 self.scale,
479 tgt_data.pos,
480 tgt_data.body,
481 tgt_data.scale,
482 read_data,
483 )
484 };
485 let elevation = self.pos.0.z - tgt_data.pos.0.z;
486
487 if attack_data.angle_xy < 30.0
488 && (elevation > 10.0 || attack_data.dist_sqrd > PREF_DIST.powi(2))
489 && line_of_sight_with_target()
490 {
491 controller.push_basic_input(InputKind::Primary);
492 } else if attack_data.dist_sqrd < RETREAT_DIST.powi(2) {
493 if let Some((bearing, _)) = agent.chaser.chase(
495 &*read_data.terrain,
496 self.pos.0,
497 self.vel.0,
498 tgt_data.pos.0,
499 TraversalConfig {
500 min_tgt_dist: 1.25,
501 ..self.traversal_config
502 },
503 ) {
504 let flee_dir = -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero);
505 let pos = self.pos.0.xy().with_z(self.pos.0.z + 1.5);
506 if read_data
507 .terrain
508 .ray(pos, pos + flee_dir * 2.0)
509 .until(|b| b.is_solid() || b.get_sprite().is_none())
510 .cast()
511 .0
512 > 1.0
513 {
514 controller.inputs.move_dir = flee_dir;
516 if !self.char_state.is_attack() {
517 controller.inputs.look_dir = -controller.inputs.look_dir;
518 }
519 } else {
520 controller.push_basic_input(InputKind::Primary);
522 }
523 }
524 } else if attack_data.dist_sqrd < PREF_DIST.powi(2) {
525 if let Some((bearing, _)) = agent.chaser.chase(
527 &*read_data.terrain,
528 self.pos.0,
529 self.vel.0,
530 tgt_data.pos.0,
531 TraversalConfig {
532 min_tgt_dist: 1.25,
533 ..self.traversal_config
534 },
535 ) {
536 if line_of_sight_with_target() {
537 controller.push_basic_input(InputKind::Primary);
538 }
539 controller.inputs.move_dir =
540 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero);
541 }
542 } else {
543 self.path_toward_target(
544 agent,
545 controller,
546 tgt_data.pos.0,
547 read_data,
548 Path::Full,
549 None,
550 );
551 }
552 }
553
554 pub fn handle_hammer_attack(
555 &self,
556 agent: &mut Agent,
557 controller: &mut Controller,
558 attack_data: &AttackData,
559 tgt_data: &TargetData,
560 read_data: &ReadData,
561 rng: &mut impl Rng,
562 ) {
563 if !agent.combat_state.initialized {
564 agent.combat_state.initialized = true;
565 let available_tactics = {
566 let mut tactics = Vec::new();
567 let try_tactic = |skill, tactic, tactics: &mut Vec<HammerTactics>| {
568 if self.skill_set.has_skill(Skill::Hammer(skill)) {
569 tactics.push(tactic);
570 }
571 };
572 try_tactic(
573 HammerSkill::Thunderclap,
574 HammerTactics::AttackExpert,
575 &mut tactics,
576 );
577 try_tactic(
578 HammerSkill::Judgement,
579 HammerTactics::SupportExpert,
580 &mut tactics,
581 );
582 if tactics.is_empty() {
583 try_tactic(
584 HammerSkill::IronTempest,
585 HammerTactics::AttackAdvanced,
586 &mut tactics,
587 );
588 try_tactic(
589 HammerSkill::Rampart,
590 HammerTactics::SupportAdvanced,
591 &mut tactics,
592 );
593 }
594 if tactics.is_empty() {
595 try_tactic(
596 HammerSkill::Retaliate,
597 HammerTactics::AttackIntermediate,
598 &mut tactics,
599 );
600 try_tactic(
601 HammerSkill::PileDriver,
602 HammerTactics::SupportIntermediate,
603 &mut tactics,
604 );
605 }
606 if tactics.is_empty() {
607 try_tactic(
608 HammerSkill::Tremor,
609 HammerTactics::AttackSimple,
610 &mut tactics,
611 );
612 try_tactic(
613 HammerSkill::HeavyWhorl,
614 HammerTactics::SupportSimple,
615 &mut tactics,
616 );
617 }
618 if tactics.is_empty() {
619 try_tactic(
620 HammerSkill::ScornfulSwipe,
621 HammerTactics::Simple,
622 &mut tactics,
623 );
624 }
625 if tactics.is_empty() {
626 tactics.push(HammerTactics::Unskilled);
627 }
628 tactics
629 };
630
631 let tactic = available_tactics
632 .choose(rng)
633 .copied()
634 .unwrap_or(HammerTactics::Unskilled);
635
636 agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
637
638 let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
639 let set_ability = |controller: &mut Controller, slot, skill| {
640 controller.push_event(ControlEvent::ChangeAbility {
641 slot,
642 auxiliary_key,
643 new_ability: AuxiliaryAbility::MainWeapon(skill),
644 });
645 };
646 let mut set_random = |controller: &mut Controller, slot, options: &mut Vec<usize>| {
647 if options.is_empty() {
648 return;
649 }
650 let i = rng.gen_range(0..options.len());
651 set_ability(controller, slot, options.swap_remove(i));
652 };
653
654 match tactic {
655 HammerTactics::Unskilled => {},
656 HammerTactics::Simple => {
657 set_ability(controller, 0, 0);
659 },
660 HammerTactics::AttackSimple => {
661 set_ability(controller, 0, 0);
663 set_ability(controller, 1, rng.gen_range(1..3));
665 },
666 HammerTactics::AttackIntermediate => {
667 set_ability(controller, 0, 0);
669 set_ability(controller, 1, rng.gen_range(1..3));
671 set_ability(controller, 2, rng.gen_range(3..6));
673 },
674 HammerTactics::AttackAdvanced => {
675 let mut options = vec![0, 1, 2, 3, 4, 5];
677 set_random(controller, 0, &mut options);
678 set_random(controller, 1, &mut options);
679 set_random(controller, 2, &mut options);
680 set_ability(controller, 3, rng.gen_range(6..8));
681 },
682 HammerTactics::AttackExpert => {
683 let mut options = vec![0, 1, 2, 3, 4, 5, 6, 7];
686 set_random(controller, 0, &mut options);
687 set_random(controller, 1, &mut options);
688 set_random(controller, 2, &mut options);
689 set_random(controller, 3, &mut options);
690 set_ability(controller, 4, rng.gen_range(8..10));
691 },
692 HammerTactics::SupportSimple => {
693 set_ability(controller, 0, 0);
695 set_ability(controller, 1, rng.gen_range(10..12));
697 },
698 HammerTactics::SupportIntermediate => {
699 set_ability(controller, 0, 0);
701 set_ability(controller, 1, rng.gen_range(10..12));
703 set_ability(controller, 2, rng.gen_range(12..15));
705 },
706 HammerTactics::SupportAdvanced => {
707 let mut options = vec![0, 10, 11, 12, 13, 14];
710 set_random(controller, 0, &mut options);
711 set_random(controller, 1, &mut options);
712 set_random(controller, 2, &mut options);
713 set_ability(controller, 3, rng.gen_range(15..17));
714 },
715 HammerTactics::SupportExpert => {
716 let mut options = vec![0, 10, 11, 12, 13, 14, 15, 16];
719 set_random(controller, 0, &mut options);
720 set_random(controller, 1, &mut options);
721 set_random(controller, 2, &mut options);
722 set_random(controller, 3, &mut options);
723 set_ability(controller, 4, rng.gen_range(17..19));
724 },
725 }
726
727 agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
728 ActionMode::Reckless as u8;
729 }
730
731 enum IntCounters {
732 Tactic = 0,
733 ActionMode = 1,
734 }
735
736 enum Timers {
737 GuardedCycle = 0,
738 PosTimeOut = 1,
739 }
740
741 enum Conditions {
742 GuardedDefend = 0,
743 RollingBreakThrough = 1,
744 }
745
746 enum FloatCounters {
747 GuardedTimer = 0,
748 }
749
750 enum Positions {
751 GuardedCover = 0,
752 Flee = 1,
753 }
754
755 let attempt_attack = handle_attack_aggression(
756 self,
757 agent,
758 controller,
759 attack_data,
760 tgt_data,
761 read_data,
762 rng,
763 Timers::PosTimeOut as usize,
764 Timers::GuardedCycle as usize,
765 FloatCounters::GuardedTimer as usize,
766 IntCounters::ActionMode as usize,
767 Conditions::GuardedDefend as usize,
768 Conditions::RollingBreakThrough as usize,
769 Positions::GuardedCover as usize,
770 Positions::Flee as usize,
771 );
772
773 let attack_failed = if attempt_attack {
774 let primary = self.extract_ability(AbilityInput::Primary);
775 let secondary = self.extract_ability(AbilityInput::Secondary);
776 let abilities = [
777 self.extract_ability(AbilityInput::Auxiliary(0)),
778 self.extract_ability(AbilityInput::Auxiliary(1)),
779 self.extract_ability(AbilityInput::Auxiliary(2)),
780 self.extract_ability(AbilityInput::Auxiliary(3)),
781 self.extract_ability(AbilityInput::Auxiliary(4)),
782 ];
783 let could_use_input = |input, desired_energy| match input {
784 InputKind::Primary => primary.as_ref().is_some_and(|p| {
785 p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
786 }),
787 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
788 s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
789 }),
790 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
791 let ability = self.active_abilities.get_ability(
792 AbilityInput::Auxiliary(x),
793 Some(self.inventory),
794 Some(self.skill_set),
795 self.stats,
796 );
797 let additional_conditions = match ability {
798 Ability::MainWeaponAux(0) => self
799 .buffs
800 .is_some_and(|buffs| !buffs.contains(BuffKind::ScornfulTaunt)),
801 Ability::MainWeaponAux(2) => {
802 tgt_data.char_state.is_some_and(|cs| cs.is_stunned())
803 },
804 Ability::MainWeaponAux(4) => tgt_data.ori.is_some_and(|ori| {
805 ori.look_vec().angle_between(tgt_data.pos.0 - self.pos.0)
806 < combat::BEHIND_TARGET_ANGLE
807 }),
808 Ability::MainWeaponAux(5) => tgt_data.char_state.is_some_and(|cs| {
809 cs.is_block(AttackSource::Melee) || cs.is_parry(AttackSource::Melee)
810 }),
811 Ability::MainWeaponAux(7) => tgt_data
812 .buffs
813 .is_some_and(|buffs| !buffs.contains(BuffKind::Staggered)),
814 Ability::MainWeaponAux(12) => tgt_data
815 .buffs
816 .is_some_and(|buffs| !buffs.contains(BuffKind::Rooted)),
817 Ability::MainWeaponAux(13) => tgt_data
818 .buffs
819 .is_some_and(|buffs| !buffs.contains(BuffKind::Winded)),
820 Ability::MainWeaponAux(14) => tgt_data
821 .buffs
822 .is_some_and(|buffs| !buffs.contains(BuffKind::Concussion)),
823 Ability::MainWeaponAux(15) => self
824 .buffs
825 .is_some_and(|buffs| !buffs.contains(BuffKind::ProtectingWard)),
826 _ => true,
827 };
828 a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
829 && additional_conditions
830 }),
831 _ => false,
832 };
833 let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
834 if matches!(current_input, InputKind::Secondary) {
835 let charging =
836 matches!(self.char_state.stage_section(), Some(StageSection::Charge));
837 let charged = self
838 .char_state
839 .durations()
840 .and_then(|durs| durs.charge)
841 .zip(self.char_state.timer())
842 .is_some_and(|(dur, timer)| timer > dur);
843 if !(charging && charged) {
844 *next_input = Some(InputKind::Secondary);
845 }
846 } else {
847 *next_input = Some(current_input);
848 }
849 };
850 let current_input = self.char_state.ability_info().map(|ai| ai.input);
851 let ability_preferences = AbilityPreferences {
852 desired_energy: 40.0,
853 combo_scaling_buildup: 0,
854 };
855 let mut next_input = None;
856 if let Some(input) = current_input {
857 continue_current_input(input, &mut next_input);
858 } else {
859 match HammerTactics::from_u8(
860 agent.combat_state.int_counters[IntCounters::Tactic as usize],
861 ) {
862 HammerTactics::Unskilled => {
863 if rng.gen_bool(0.5) {
864 next_input = Some(InputKind::Primary);
865 } else {
866 next_input = Some(InputKind::Secondary);
867 }
868 },
869 HammerTactics::Simple => {
870 if rng.gen_bool(0.5) {
871 next_input = Some(InputKind::Primary);
872 } else {
873 next_input = Some(InputKind::Secondary);
874 }
875 },
876 HammerTactics::AttackSimple | HammerTactics::SupportSimple => {
877 if could_use_input(InputKind::Ability(0), ability_preferences) {
878 next_input = Some(InputKind::Ability(0));
879 } else if rng.gen_bool(0.5) {
880 next_input = Some(InputKind::Primary);
881 } else {
882 next_input = Some(InputKind::Secondary);
883 }
884 },
885 HammerTactics::AttackIntermediate | HammerTactics::SupportIntermediate => {
886 let random_ability = InputKind::Ability(rng.gen_range(0..3));
887 if could_use_input(random_ability, ability_preferences) {
888 next_input = Some(random_ability);
889 } else if rng.gen_bool(0.5) {
890 next_input = Some(InputKind::Primary);
891 } else {
892 next_input = Some(InputKind::Secondary);
893 }
894 },
895 HammerTactics::AttackAdvanced | HammerTactics::SupportAdvanced => {
896 let random_ability = InputKind::Ability(rng.gen_range(0..5));
897 if could_use_input(random_ability, ability_preferences) {
898 next_input = Some(random_ability);
899 } else if rng.gen_bool(0.5) {
900 next_input = Some(InputKind::Primary);
901 } else {
902 next_input = Some(InputKind::Secondary);
903 }
904 },
905 HammerTactics::AttackExpert | HammerTactics::SupportExpert => {
906 let random_ability = InputKind::Ability(rng.gen_range(0..5));
907 if could_use_input(random_ability, ability_preferences) {
908 next_input = Some(random_ability);
909 } else if rng.gen_bool(0.5) {
910 next_input = Some(InputKind::Primary);
911 } else {
912 next_input = Some(InputKind::Secondary);
913 }
914 },
915 }
916 }
917 if let Some(input) = next_input {
918 if could_use_input(input, ability_preferences) {
919 controller.push_basic_input(input);
920 false
921 } else {
922 true
923 }
924 } else {
925 true
926 }
927 } else {
928 false
929 };
930
931 if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
932 self.path_toward_target(
933 agent,
934 controller,
935 tgt_data.pos.0,
936 read_data,
937 Path::Separate,
938 None,
939 );
940 }
941 }
942
943 pub fn handle_sword_attack(
944 &self,
945 agent: &mut Agent,
946 controller: &mut Controller,
947 attack_data: &AttackData,
948 tgt_data: &TargetData,
949 read_data: &ReadData,
950 rng: &mut impl Rng,
951 ) {
952 if !agent.combat_state.initialized {
953 agent.combat_state.initialized = true;
954 let available_tactics = {
955 let mut tactics = Vec::new();
956 let try_tactic = |skill, tactic, tactics: &mut Vec<SwordTactics>| {
957 if self.skill_set.has_skill(Skill::Sword(skill)) {
958 tactics.push(tactic);
959 }
960 };
961 try_tactic(
962 SwordSkill::HeavyFortitude,
963 SwordTactics::HeavyAdvanced,
964 &mut tactics,
965 );
966 try_tactic(
967 SwordSkill::AgileDancingEdge,
968 SwordTactics::AgileAdvanced,
969 &mut tactics,
970 );
971 try_tactic(
972 SwordSkill::DefensiveStalwartSword,
973 SwordTactics::DefensiveAdvanced,
974 &mut tactics,
975 );
976 try_tactic(
977 SwordSkill::CripplingEviscerate,
978 SwordTactics::CripplingAdvanced,
979 &mut tactics,
980 );
981 try_tactic(
982 SwordSkill::CleavingBladeFever,
983 SwordTactics::CleavingAdvanced,
984 &mut tactics,
985 );
986 if tactics.is_empty() {
987 try_tactic(
988 SwordSkill::HeavySweep,
989 SwordTactics::HeavySimple,
990 &mut tactics,
991 );
992 try_tactic(
993 SwordSkill::AgileQuickDraw,
994 SwordTactics::AgileSimple,
995 &mut tactics,
996 );
997 try_tactic(
998 SwordSkill::DefensiveDisengage,
999 SwordTactics::DefensiveSimple,
1000 &mut tactics,
1001 );
1002 try_tactic(
1003 SwordSkill::CripplingGouge,
1004 SwordTactics::CripplingSimple,
1005 &mut tactics,
1006 );
1007 try_tactic(
1008 SwordSkill::CleavingWhirlwindSlice,
1009 SwordTactics::CleavingSimple,
1010 &mut tactics,
1011 );
1012 }
1013 if tactics.is_empty() {
1014 try_tactic(SwordSkill::CrescentSlash, SwordTactics::Basic, &mut tactics);
1015 }
1016 if tactics.is_empty() {
1017 tactics.push(SwordTactics::Unskilled);
1018 }
1019 tactics
1020 };
1021
1022 let tactic = available_tactics
1023 .choose(rng)
1024 .copied()
1025 .unwrap_or(SwordTactics::Unskilled);
1026
1027 agent.combat_state.int_counters[IntCounters::Tactics as usize] = tactic as u8;
1028
1029 let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
1030 let set_sword_ability = |controller: &mut Controller, slot, skill| {
1031 controller.push_event(ControlEvent::ChangeAbility {
1032 slot,
1033 auxiliary_key,
1034 new_ability: AuxiliaryAbility::MainWeapon(skill),
1035 });
1036 };
1037
1038 match tactic {
1039 SwordTactics::Unskilled => {},
1040 SwordTactics::Basic => {
1041 set_sword_ability(controller, 0, 0);
1043 set_sword_ability(controller, 1, 1);
1045 set_sword_ability(controller, 2, 2);
1047 set_sword_ability(controller, 3, 3);
1049 set_sword_ability(controller, 4, 4);
1051 },
1052 SwordTactics::HeavySimple => {
1053 set_sword_ability(controller, 0, 5);
1055 set_sword_ability(controller, 1, 0);
1057 set_sword_ability(controller, 2, 3);
1059 set_sword_ability(controller, 3, 6);
1061 set_sword_ability(controller, 4, 7);
1063 },
1064 SwordTactics::AgileSimple => {
1065 set_sword_ability(controller, 0, 5);
1067 set_sword_ability(controller, 1, 2);
1069 set_sword_ability(controller, 2, 4);
1071 set_sword_ability(controller, 3, 8);
1073 set_sword_ability(controller, 4, 9);
1075 },
1076 SwordTactics::DefensiveSimple => {
1077 set_sword_ability(controller, 0, 5);
1079 set_sword_ability(controller, 1, 0);
1081 set_sword_ability(controller, 2, 1);
1083 set_sword_ability(controller, 3, 10);
1085 set_sword_ability(controller, 4, 11);
1087 },
1088 SwordTactics::CripplingSimple => {
1089 set_sword_ability(controller, 0, 5);
1091 set_sword_ability(controller, 1, 1);
1093 set_sword_ability(controller, 2, 2);
1095 set_sword_ability(controller, 3, 12);
1097 set_sword_ability(controller, 4, 13);
1099 },
1100 SwordTactics::CleavingSimple => {
1101 set_sword_ability(controller, 0, 5);
1103 set_sword_ability(controller, 1, 3);
1105 set_sword_ability(controller, 2, 4);
1107 set_sword_ability(controller, 3, 14);
1109 set_sword_ability(controller, 4, 15);
1111 },
1112 SwordTactics::HeavyAdvanced => {
1113 set_sword_ability(controller, 0, 5);
1115 set_sword_ability(controller, 1, 6);
1117 set_sword_ability(controller, 2, 7);
1119 set_sword_ability(controller, 3, 16);
1121 set_sword_ability(controller, 4, 17);
1123 },
1124 SwordTactics::AgileAdvanced => {
1125 set_sword_ability(controller, 0, 5);
1127 set_sword_ability(controller, 1, 8);
1129 set_sword_ability(controller, 2, 9);
1131 set_sword_ability(controller, 3, 18);
1133 set_sword_ability(controller, 4, 19);
1135 },
1136 SwordTactics::DefensiveAdvanced => {
1137 set_sword_ability(controller, 0, 5);
1139 set_sword_ability(controller, 1, 10);
1141 set_sword_ability(controller, 2, 11);
1143 set_sword_ability(controller, 3, 20);
1145 set_sword_ability(controller, 4, 21);
1147 },
1148 SwordTactics::CripplingAdvanced => {
1149 set_sword_ability(controller, 0, 5);
1151 set_sword_ability(controller, 1, 12);
1153 set_sword_ability(controller, 2, 13);
1155 set_sword_ability(controller, 3, 22);
1157 set_sword_ability(controller, 4, 23);
1159 },
1160 SwordTactics::CleavingAdvanced => {
1161 set_sword_ability(controller, 0, 5);
1163 set_sword_ability(controller, 1, 14);
1165 set_sword_ability(controller, 2, 15);
1167 set_sword_ability(controller, 3, 24);
1169 set_sword_ability(controller, 4, 25);
1171 },
1172 }
1173
1174 agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
1175 ActionMode::Reckless as u8;
1176 }
1177
1178 enum IntCounters {
1179 Tactics = 0,
1180 ActionMode = 1,
1181 }
1182
1183 enum Timers {
1184 GuardedCycle = 0,
1185 PosTimeOut = 1,
1186 }
1187
1188 enum Conditions {
1189 GuardedDefend = 0,
1190 RollingBreakThrough = 1,
1191 }
1192
1193 enum FloatCounters {
1194 GuardedTimer = 0,
1195 }
1196
1197 enum Positions {
1198 GuardedCover = 0,
1199 Flee = 1,
1200 }
1201
1202 let attempt_attack = handle_attack_aggression(
1203 self,
1204 agent,
1205 controller,
1206 attack_data,
1207 tgt_data,
1208 read_data,
1209 rng,
1210 Timers::PosTimeOut as usize,
1211 Timers::GuardedCycle as usize,
1212 FloatCounters::GuardedTimer as usize,
1213 IntCounters::ActionMode as usize,
1214 Conditions::GuardedDefend as usize,
1215 Conditions::RollingBreakThrough as usize,
1216 Positions::GuardedCover as usize,
1217 Positions::Flee as usize,
1218 );
1219
1220 let attack_failed = if attempt_attack {
1221 let primary = self.extract_ability(AbilityInput::Primary);
1222 let secondary = self.extract_ability(AbilityInput::Secondary);
1223 let abilities = [
1224 self.extract_ability(AbilityInput::Auxiliary(0)),
1225 self.extract_ability(AbilityInput::Auxiliary(1)),
1226 self.extract_ability(AbilityInput::Auxiliary(2)),
1227 self.extract_ability(AbilityInput::Auxiliary(3)),
1228 self.extract_ability(AbilityInput::Auxiliary(4)),
1229 ];
1230 let could_use_input = |input, desired_energy| match input {
1231 InputKind::Primary => primary.as_ref().is_some_and(|p| {
1232 p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1233 }),
1234 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
1235 s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1236 }),
1237 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
1238 a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1239 }),
1240 _ => false,
1241 };
1242 let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
1243 if matches!(current_input, InputKind::Secondary) {
1244 let charging =
1245 matches!(self.char_state.stage_section(), Some(StageSection::Charge));
1246 let charged = self
1247 .char_state
1248 .durations()
1249 .and_then(|durs| durs.charge)
1250 .zip(self.char_state.timer())
1251 .is_some_and(|(dur, timer)| timer > dur);
1252 if !(charging && charged) {
1253 *next_input = Some(InputKind::Secondary);
1254 }
1255 } else {
1256 *next_input = Some(current_input);
1257 }
1258 };
1259 match SwordTactics::from_u8(
1260 agent.combat_state.int_counters[IntCounters::Tactics as usize],
1261 ) {
1262 SwordTactics::Unskilled => {
1263 let ability_preferences = AbilityPreferences {
1264 desired_energy: 15.0,
1265 combo_scaling_buildup: 0,
1266 };
1267 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1268 let mut next_input = None;
1269 if let Some(input) = current_input {
1270 continue_current_input(input, &mut next_input);
1271 } else if rng.gen_bool(0.5) {
1272 next_input = Some(InputKind::Primary);
1273 } else {
1274 next_input = Some(InputKind::Secondary);
1275 };
1276 if let Some(input) = next_input {
1277 if could_use_input(input, ability_preferences) {
1278 controller.push_basic_input(input);
1279 false
1280 } else {
1281 true
1282 }
1283 } else {
1284 true
1285 }
1286 },
1287 SwordTactics::Basic => {
1288 let ability_preferences = AbilityPreferences {
1289 desired_energy: 25.0,
1290 combo_scaling_buildup: 0,
1291 };
1292 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1293 let mut next_input = None;
1294 if let Some(input) = current_input {
1295 continue_current_input(input, &mut next_input);
1296 } else {
1297 let attempt_ability = InputKind::Ability(rng.gen_range(0..5));
1298 if could_use_input(attempt_ability, ability_preferences) {
1299 next_input = Some(attempt_ability);
1300 } else if rng.gen_bool(0.5) {
1301 next_input = Some(InputKind::Primary);
1302 } else {
1303 next_input = Some(InputKind::Secondary);
1304 }
1305 };
1306 if let Some(input) = next_input {
1307 if could_use_input(input, ability_preferences) {
1308 controller.push_basic_input(input);
1309 false
1310 } else {
1311 true
1312 }
1313 } else {
1314 true
1315 }
1316 },
1317 SwordTactics::HeavySimple => {
1318 let ability_preferences = AbilityPreferences {
1319 desired_energy: 35.0,
1320 combo_scaling_buildup: 0,
1321 };
1322 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1323 let mut next_input = None;
1324 if let Some(input) = current_input {
1325 continue_current_input(input, &mut next_input);
1326 } else {
1327 let stance_ability = InputKind::Ability(rng.gen_range(3..5));
1328 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1329 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) {
1330 if could_use_input(stance_ability, ability_preferences) {
1331 next_input = Some(stance_ability);
1332 } else if rng.gen_bool(0.5) {
1333 next_input = Some(InputKind::Primary);
1334 } else {
1335 next_input = Some(InputKind::Secondary);
1336 }
1337 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1338 next_input = Some(InputKind::Ability(0));
1339 } else if could_use_input(random_ability, ability_preferences) {
1340 next_input = Some(random_ability);
1341 } else if rng.gen_bool(0.5) {
1342 next_input = Some(InputKind::Primary);
1343 } else {
1344 next_input = Some(InputKind::Secondary);
1345 }
1346 };
1347 if let Some(input) = next_input {
1348 if could_use_input(input, ability_preferences) {
1349 controller.push_basic_input(input);
1350 false
1351 } else {
1352 true
1353 }
1354 } else {
1355 true
1356 }
1357 },
1358 SwordTactics::AgileSimple => {
1359 let ability_preferences = AbilityPreferences {
1360 desired_energy: 35.0,
1361 combo_scaling_buildup: 0,
1362 };
1363 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1364 let mut next_input = None;
1365 if let Some(input) = current_input {
1366 continue_current_input(input, &mut next_input);
1367 } else {
1368 let stance_ability = InputKind::Ability(rng.gen_range(3..5));
1369 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1370 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) {
1371 if could_use_input(stance_ability, ability_preferences) {
1372 next_input = Some(stance_ability);
1373 } else if rng.gen_bool(0.5) {
1374 next_input = Some(InputKind::Primary);
1375 } else {
1376 next_input = Some(InputKind::Secondary);
1377 }
1378 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1379 next_input = Some(InputKind::Ability(0));
1380 } else if could_use_input(random_ability, ability_preferences) {
1381 next_input = Some(random_ability);
1382 } else if rng.gen_bool(0.5) {
1383 next_input = Some(InputKind::Primary);
1384 } else {
1385 next_input = Some(InputKind::Secondary);
1386 }
1387 };
1388 if let Some(input) = next_input {
1389 if could_use_input(input, ability_preferences) {
1390 controller.push_basic_input(input);
1391 false
1392 } else {
1393 true
1394 }
1395 } else {
1396 true
1397 }
1398 },
1399 SwordTactics::DefensiveSimple => {
1400 let ability_preferences = AbilityPreferences {
1401 desired_energy: 35.0,
1402 combo_scaling_buildup: 0,
1403 };
1404 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1405 let mut next_input = None;
1406 if let Some(input) = current_input {
1407 continue_current_input(input, &mut next_input);
1408 } else {
1409 let stance_ability = InputKind::Ability(rng.gen_range(3..5));
1410 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1411 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) {
1412 if could_use_input(stance_ability, ability_preferences) {
1413 next_input = Some(stance_ability);
1414 } else if rng.gen_bool(0.5) {
1415 next_input = Some(InputKind::Primary);
1416 } else {
1417 next_input = Some(InputKind::Secondary);
1418 }
1419 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1420 next_input = Some(InputKind::Ability(0));
1421 } else if could_use_input(InputKind::Ability(3), ability_preferences) {
1422 next_input = Some(InputKind::Ability(3));
1423 } else if could_use_input(random_ability, ability_preferences) {
1424 next_input = Some(random_ability);
1425 } else if rng.gen_bool(0.5) {
1426 next_input = Some(InputKind::Primary);
1427 } else {
1428 next_input = Some(InputKind::Secondary);
1429 }
1430 };
1431 if let Some(input) = next_input {
1432 if could_use_input(input, ability_preferences) {
1433 controller.push_basic_input(input);
1434 false
1435 } else {
1436 true
1437 }
1438 } else {
1439 true
1440 }
1441 },
1442 SwordTactics::CripplingSimple => {
1443 let ability_preferences = AbilityPreferences {
1444 desired_energy: 35.0,
1445 combo_scaling_buildup: 0,
1446 };
1447 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1448 let mut next_input = None;
1449 if let Some(input) = current_input {
1450 continue_current_input(input, &mut next_input);
1451 } else {
1452 let stance_ability = InputKind::Ability(rng.gen_range(3..5));
1453 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1454 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) {
1455 if could_use_input(stance_ability, ability_preferences) {
1456 next_input = Some(stance_ability);
1457 } else if rng.gen_bool(0.5) {
1458 next_input = Some(InputKind::Primary);
1459 } else {
1460 next_input = Some(InputKind::Secondary);
1461 }
1462 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1463 next_input = Some(InputKind::Ability(0));
1464 } else if could_use_input(random_ability, ability_preferences) {
1465 next_input = Some(random_ability);
1466 } else if rng.gen_bool(0.5) {
1467 next_input = Some(InputKind::Primary);
1468 } else {
1469 next_input = Some(InputKind::Secondary);
1470 }
1471 };
1472 if let Some(input) = next_input {
1473 if could_use_input(input, ability_preferences) {
1474 controller.push_basic_input(input);
1475 false
1476 } else {
1477 true
1478 }
1479 } else {
1480 true
1481 }
1482 },
1483 SwordTactics::CleavingSimple => {
1484 let ability_preferences = AbilityPreferences {
1485 desired_energy: 35.0,
1486 combo_scaling_buildup: 0,
1487 };
1488 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1489 let mut next_input = None;
1490 if let Some(input) = current_input {
1491 continue_current_input(input, &mut next_input);
1492 } else {
1493 let stance_ability = InputKind::Ability(rng.gen_range(3..5));
1494 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1495 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) {
1496 if could_use_input(stance_ability, ability_preferences) {
1497 next_input = Some(stance_ability);
1498 } else if rng.gen_bool(0.5) {
1499 next_input = Some(InputKind::Primary);
1500 } else {
1501 next_input = Some(InputKind::Secondary);
1502 }
1503 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1504 next_input = Some(InputKind::Ability(0));
1505 } else if could_use_input(random_ability, ability_preferences) {
1506 next_input = Some(random_ability);
1507 } else if rng.gen_bool(0.5) {
1508 next_input = Some(InputKind::Primary);
1509 } else {
1510 next_input = Some(InputKind::Secondary);
1511 }
1512 };
1513 if let Some(input) = next_input {
1514 if could_use_input(input, ability_preferences) {
1515 controller.push_basic_input(input);
1516 false
1517 } else {
1518 true
1519 }
1520 } else {
1521 true
1522 }
1523 },
1524 SwordTactics::HeavyAdvanced => {
1525 let ability_preferences = AbilityPreferences {
1526 desired_energy: 50.0,
1527 combo_scaling_buildup: 0,
1528 };
1529 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1530 let mut next_input = None;
1531 if let Some(input) = current_input {
1532 continue_current_input(input, &mut next_input);
1533 } else {
1534 let stance_ability = InputKind::Ability(rng.gen_range(1..3));
1535 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1536 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) {
1537 if could_use_input(stance_ability, ability_preferences) {
1538 next_input = Some(stance_ability);
1539 } else if rng.gen_bool(0.5) {
1540 next_input = Some(InputKind::Primary);
1541 } else {
1542 next_input = Some(InputKind::Secondary);
1543 }
1544 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1545 next_input = Some(InputKind::Ability(0));
1546 } else if could_use_input(random_ability, ability_preferences) {
1547 next_input = Some(random_ability);
1548 } else if rng.gen_bool(0.5) {
1549 next_input = Some(InputKind::Primary);
1550 } else {
1551 next_input = Some(InputKind::Secondary);
1552 }
1553 };
1554 if let Some(input) = next_input {
1555 if could_use_input(input, ability_preferences) {
1556 controller.push_basic_input(input);
1557 false
1558 } else {
1559 true
1560 }
1561 } else {
1562 true
1563 }
1564 },
1565 SwordTactics::AgileAdvanced => {
1566 let ability_preferences = AbilityPreferences {
1567 desired_energy: 50.0,
1568 combo_scaling_buildup: 0,
1569 };
1570 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1571 let mut next_input = None;
1572 if let Some(input) = current_input {
1573 continue_current_input(input, &mut next_input);
1574 } else {
1575 let stance_ability = InputKind::Ability(rng.gen_range(1..3));
1576 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1577 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) {
1578 if could_use_input(stance_ability, ability_preferences) {
1579 next_input = Some(stance_ability);
1580 } else if rng.gen_bool(0.5) {
1581 next_input = Some(InputKind::Primary);
1582 } else {
1583 next_input = Some(InputKind::Secondary);
1584 }
1585 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1586 next_input = Some(InputKind::Ability(0));
1587 } else if could_use_input(random_ability, ability_preferences) {
1588 next_input = Some(random_ability);
1589 } else if rng.gen_bool(0.5) {
1590 next_input = Some(InputKind::Primary);
1591 } else {
1592 next_input = Some(InputKind::Secondary);
1593 }
1594 };
1595 if let Some(input) = next_input {
1596 if could_use_input(input, ability_preferences) {
1597 controller.push_basic_input(input);
1598 false
1599 } else {
1600 true
1601 }
1602 } else {
1603 true
1604 }
1605 },
1606 SwordTactics::DefensiveAdvanced => {
1607 let ability_preferences = AbilityPreferences {
1608 desired_energy: 50.0,
1609 combo_scaling_buildup: 0,
1610 };
1611 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1612 let mut next_input = None;
1613 if let Some(input) = current_input {
1614 continue_current_input(input, &mut next_input);
1615 } else {
1616 let stance_ability = InputKind::Ability(rng.gen_range(1..3));
1617 let random_ability = InputKind::Ability(rng.gen_range(1..4));
1618 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) {
1619 if could_use_input(stance_ability, ability_preferences) {
1620 next_input = Some(stance_ability);
1621 } else if rng.gen_bool(0.5) {
1622 next_input = Some(InputKind::Primary);
1623 } else {
1624 next_input = Some(InputKind::Secondary);
1625 }
1626 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1627 next_input = Some(InputKind::Ability(0));
1628 } else if could_use_input(random_ability, ability_preferences) {
1629 next_input = Some(random_ability);
1630 } else if could_use_input(InputKind::Ability(4), ability_preferences)
1631 && rng.gen_bool(2.0 * read_data.dt.0 as f64)
1632 {
1633 next_input = Some(InputKind::Ability(4));
1634 } else if rng.gen_bool(0.5) {
1635 next_input = Some(InputKind::Primary);
1636 } else {
1637 next_input = Some(InputKind::Secondary);
1638 }
1639 };
1640 if let Some(input) = next_input {
1641 if could_use_input(input, ability_preferences) {
1642 controller.push_basic_input(input);
1643 false
1644 } else {
1645 true
1646 }
1647 } else {
1648 true
1649 }
1650 },
1651 SwordTactics::CripplingAdvanced => {
1652 let ability_preferences = AbilityPreferences {
1653 desired_energy: 50.0,
1654 combo_scaling_buildup: 0,
1655 };
1656 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1657 let mut next_input = None;
1658 if let Some(input) = current_input {
1659 continue_current_input(input, &mut next_input);
1660 } else {
1661 let stance_ability = InputKind::Ability(rng.gen_range(1..3));
1662 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1663 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) {
1664 if could_use_input(stance_ability, ability_preferences) {
1665 next_input = Some(stance_ability);
1666 } else if rng.gen_bool(0.5) {
1667 next_input = Some(InputKind::Primary);
1668 } else {
1669 next_input = Some(InputKind::Secondary);
1670 }
1671 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1672 next_input = Some(InputKind::Ability(0));
1673 } else if could_use_input(random_ability, ability_preferences) {
1674 next_input = Some(random_ability);
1675 } else if rng.gen_bool(0.5) {
1676 next_input = Some(InputKind::Primary);
1677 } else {
1678 next_input = Some(InputKind::Secondary);
1679 }
1680 };
1681 if let Some(input) = next_input {
1682 if could_use_input(input, ability_preferences) {
1683 controller.push_basic_input(input);
1684 false
1685 } else {
1686 true
1687 }
1688 } else {
1689 true
1690 }
1691 },
1692 SwordTactics::CleavingAdvanced => {
1693 let ability_preferences = AbilityPreferences {
1694 desired_energy: 50.0,
1695 combo_scaling_buildup: 0,
1696 };
1697 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1698 let mut next_input = None;
1699 if let Some(input) = current_input {
1700 continue_current_input(input, &mut next_input);
1701 } else {
1702 let stance_ability = InputKind::Ability(rng.gen_range(1..3));
1703 let random_ability = InputKind::Ability(rng.gen_range(1..5));
1704 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) {
1705 if could_use_input(stance_ability, ability_preferences) {
1706 next_input = Some(stance_ability);
1707 } else if rng.gen_bool(0.5) {
1708 next_input = Some(InputKind::Primary);
1709 } else {
1710 next_input = Some(InputKind::Secondary);
1711 }
1712 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1713 next_input = Some(InputKind::Ability(0));
1714 } else if could_use_input(random_ability, ability_preferences) {
1715 next_input = Some(random_ability);
1716 } else if rng.gen_bool(0.5) {
1717 next_input = Some(InputKind::Primary);
1718 } else {
1719 next_input = Some(InputKind::Secondary);
1720 }
1721 };
1722 if let Some(input) = next_input {
1723 if could_use_input(input, ability_preferences) {
1724 controller.push_basic_input(input);
1725 false
1726 } else {
1727 true
1728 }
1729 } else {
1730 true
1731 }
1732 },
1733 }
1734 } else {
1735 false
1736 };
1737
1738 if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
1739 self.path_toward_target(
1740 agent,
1741 controller,
1742 tgt_data.pos.0,
1743 read_data,
1744 Path::Separate,
1745 None,
1746 );
1747 }
1748 }
1749
1750 pub fn handle_axe_attack(
1751 &self,
1752 agent: &mut Agent,
1753 controller: &mut Controller,
1754 attack_data: &AttackData,
1755 tgt_data: &TargetData,
1756 read_data: &ReadData,
1757 rng: &mut impl Rng,
1758 ) {
1759 if !agent.combat_state.initialized {
1760 agent.combat_state.initialized = true;
1761 let available_tactics = {
1762 let mut tactics = Vec::new();
1763 let try_tactic = |skill, tactic, tactics: &mut Vec<AxeTactics>| {
1764 if self.skill_set.has_skill(Skill::Axe(skill)) {
1765 tactics.push(tactic);
1766 }
1767 };
1768 try_tactic(AxeSkill::Execute, AxeTactics::SavageAdvanced, &mut tactics);
1769 try_tactic(
1770 AxeSkill::Lacerate,
1771 AxeTactics::MercilessAdvanced,
1772 &mut tactics,
1773 );
1774 try_tactic(AxeSkill::Bulkhead, AxeTactics::RivingAdvanced, &mut tactics);
1775 if tactics.is_empty() {
1776 try_tactic(
1777 AxeSkill::RisingTide,
1778 AxeTactics::SavageIntermediate,
1779 &mut tactics,
1780 );
1781 try_tactic(
1782 AxeSkill::FierceRaze,
1783 AxeTactics::MercilessIntermediate,
1784 &mut tactics,
1785 );
1786 try_tactic(
1787 AxeSkill::Plunder,
1788 AxeTactics::RivingIntermediate,
1789 &mut tactics,
1790 );
1791 }
1792 if tactics.is_empty() {
1793 try_tactic(
1794 AxeSkill::BrutalSwing,
1795 AxeTactics::SavageSimple,
1796 &mut tactics,
1797 );
1798 try_tactic(AxeSkill::Rake, AxeTactics::MercilessSimple, &mut tactics);
1799 try_tactic(AxeSkill::SkullBash, AxeTactics::RivingSimple, &mut tactics);
1800 }
1801 if tactics.is_empty() {
1802 tactics.push(AxeTactics::Unskilled);
1803 }
1804 tactics
1805 };
1806
1807 let tactic = available_tactics
1808 .choose(rng)
1809 .copied()
1810 .unwrap_or(AxeTactics::Unskilled);
1811
1812 agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
1813
1814 let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
1815 let set_axe_ability = |controller: &mut Controller, slot, skill| {
1816 controller.push_event(ControlEvent::ChangeAbility {
1817 slot,
1818 auxiliary_key,
1819 new_ability: AuxiliaryAbility::MainWeapon(skill),
1820 });
1821 };
1822
1823 match tactic {
1824 AxeTactics::Unskilled => {},
1825 AxeTactics::SavageSimple => {
1826 set_axe_ability(controller, 0, 0);
1828 },
1829 AxeTactics::MercilessSimple => {
1830 set_axe_ability(controller, 0, 6);
1832 },
1833 AxeTactics::RivingSimple => {
1834 set_axe_ability(controller, 0, 12);
1836 },
1837 AxeTactics::SavageIntermediate => {
1838 set_axe_ability(controller, 0, 0);
1840 set_axe_ability(controller, 1, 1);
1842 set_axe_ability(controller, 2, 2);
1844 },
1845 AxeTactics::MercilessIntermediate => {
1846 set_axe_ability(controller, 0, 6);
1848 set_axe_ability(controller, 1, 7);
1850 set_axe_ability(controller, 2, 8);
1852 },
1853 AxeTactics::RivingIntermediate => {
1854 set_axe_ability(controller, 0, 12);
1856 set_axe_ability(controller, 1, 13);
1858 set_axe_ability(controller, 2, 14);
1860 },
1861 AxeTactics::SavageAdvanced => {
1862 set_axe_ability(controller, 0, 1);
1864 set_axe_ability(controller, 1, 2);
1866 set_axe_ability(controller, 2, 3);
1868 set_axe_ability(controller, 3, 4);
1870 set_axe_ability(controller, 4, 5);
1872 },
1873 AxeTactics::MercilessAdvanced => {
1874 set_axe_ability(controller, 0, 7);
1876 set_axe_ability(controller, 1, 8);
1878 set_axe_ability(controller, 2, 9);
1880 set_axe_ability(controller, 3, 10);
1882 set_axe_ability(controller, 4, 11);
1884 },
1885 AxeTactics::RivingAdvanced => {
1886 set_axe_ability(controller, 0, 13);
1888 set_axe_ability(controller, 1, 14);
1890 set_axe_ability(controller, 2, 15);
1892 set_axe_ability(controller, 3, 16);
1894 set_axe_ability(controller, 4, 17);
1896 },
1897 }
1898
1899 agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
1900 ActionMode::Reckless as u8;
1901 }
1902
1903 enum IntCounters {
1904 Tactic = 0,
1905 ActionMode = 1,
1906 }
1907
1908 enum Timers {
1909 GuardedCycle = 0,
1910 PosTimeOut = 1,
1911 }
1912
1913 enum Conditions {
1914 GuardedDefend = 0,
1915 RollingBreakThrough = 1,
1916 }
1917
1918 enum FloatCounters {
1919 GuardedTimer = 0,
1920 }
1921
1922 enum Positions {
1923 GuardedCover = 0,
1924 Flee = 1,
1925 }
1926
1927 let attempt_attack = handle_attack_aggression(
1928 self,
1929 agent,
1930 controller,
1931 attack_data,
1932 tgt_data,
1933 read_data,
1934 rng,
1935 Timers::PosTimeOut as usize,
1936 Timers::GuardedCycle as usize,
1937 FloatCounters::GuardedTimer as usize,
1938 IntCounters::ActionMode as usize,
1939 Conditions::GuardedDefend as usize,
1940 Conditions::RollingBreakThrough as usize,
1941 Positions::GuardedCover as usize,
1942 Positions::Flee as usize,
1943 );
1944
1945 let attack_failed = if attempt_attack {
1946 let primary = self.extract_ability(AbilityInput::Primary);
1947 let secondary = self.extract_ability(AbilityInput::Secondary);
1948 let abilities = [
1949 self.extract_ability(AbilityInput::Auxiliary(0)),
1950 self.extract_ability(AbilityInput::Auxiliary(1)),
1951 self.extract_ability(AbilityInput::Auxiliary(2)),
1952 self.extract_ability(AbilityInput::Auxiliary(3)),
1953 self.extract_ability(AbilityInput::Auxiliary(4)),
1954 ];
1955 let could_use_input = |input, desired_energy| match input {
1956 InputKind::Primary => primary.as_ref().is_some_and(|p| {
1957 p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1958 }),
1959 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
1960 s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1961 }),
1962 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
1963 a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1964 }),
1965 _ => false,
1966 };
1967 let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
1968 if matches!(current_input, InputKind::Secondary) {
1969 let charging =
1970 matches!(self.char_state.stage_section(), Some(StageSection::Charge));
1971 let charged = self
1972 .char_state
1973 .durations()
1974 .and_then(|durs| durs.charge)
1975 .zip(self.char_state.timer())
1976 .is_some_and(|(dur, timer)| timer > dur);
1977 if !(charging && charged) {
1978 *next_input = Some(InputKind::Secondary);
1979 }
1980 } else {
1981 *next_input = Some(current_input);
1982 }
1983 };
1984 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1985 let ability_preferences = AbilityPreferences {
1986 desired_energy: 40.0,
1987 combo_scaling_buildup: 15,
1988 };
1989 let mut next_input = None;
1990 if let Some(input) = current_input {
1991 continue_current_input(input, &mut next_input);
1992 } else {
1993 match AxeTactics::from_u8(
1994 agent.combat_state.int_counters[IntCounters::Tactic as usize],
1995 ) {
1996 AxeTactics::Unskilled => {
1997 if rng.gen_bool(0.5) {
1998 next_input = Some(InputKind::Primary);
1999 } else {
2000 next_input = Some(InputKind::Secondary);
2001 }
2002 },
2003 AxeTactics::SavageSimple
2004 | AxeTactics::MercilessSimple
2005 | AxeTactics::RivingSimple => {
2006 if could_use_input(InputKind::Ability(0), ability_preferences) {
2007 next_input = Some(InputKind::Ability(0));
2008 } else if rng.gen_bool(0.5) {
2009 next_input = Some(InputKind::Primary);
2010 } else {
2011 next_input = Some(InputKind::Secondary);
2012 }
2013 },
2014 AxeTactics::SavageIntermediate
2015 | AxeTactics::MercilessIntermediate
2016 | AxeTactics::RivingIntermediate => {
2017 let random_ability = InputKind::Ability(rng.gen_range(0..3));
2018 if could_use_input(random_ability, ability_preferences) {
2019 next_input = Some(random_ability);
2020 } else if rng.gen_bool(0.5) {
2021 next_input = Some(InputKind::Primary);
2022 } else {
2023 next_input = Some(InputKind::Secondary);
2024 }
2025 },
2026 AxeTactics::SavageAdvanced
2027 | AxeTactics::MercilessAdvanced
2028 | AxeTactics::RivingAdvanced => {
2029 let random_ability = InputKind::Ability(rng.gen_range(0..5));
2030 if could_use_input(random_ability, ability_preferences) {
2031 next_input = Some(random_ability);
2032 } else if rng.gen_bool(0.5) {
2033 next_input = Some(InputKind::Primary);
2034 } else {
2035 next_input = Some(InputKind::Secondary);
2036 }
2037 },
2038 }
2039 }
2040 if let Some(input) = next_input {
2041 if could_use_input(input, ability_preferences) {
2042 controller.push_basic_input(input);
2043 false
2044 } else {
2045 true
2046 }
2047 } else {
2048 true
2049 }
2050 } else {
2051 false
2052 };
2053
2054 if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
2055 self.path_toward_target(
2056 agent,
2057 controller,
2058 tgt_data.pos.0,
2059 read_data,
2060 Path::Separate,
2061 None,
2062 );
2063 }
2064 }
2065
2066 pub fn handle_bow_attack(
2067 &self,
2068 agent: &mut Agent,
2069 controller: &mut Controller,
2070 attack_data: &AttackData,
2071 tgt_data: &TargetData,
2072 read_data: &ReadData,
2073 rng: &mut impl Rng,
2074 ) {
2075 const MIN_CHARGE_FRAC: f32 = 0.5;
2076 const OPTIMAL_TARGET_VELOCITY: f32 = 5.0;
2077 const DESIRED_ENERGY_LEVEL: f32 = 50.0;
2078
2079 let line_of_sight_with_target = || {
2080 entities_have_line_of_sight(
2081 self.pos,
2082 self.body,
2083 self.scale,
2084 tgt_data.pos,
2085 tgt_data.body,
2086 tgt_data.scale,
2087 read_data,
2088 )
2089 };
2090
2091 if let CharacterState::ChargedRanged(c) = self.char_state {
2093 if !matches!(c.stage_section, StageSection::Recover) {
2094 let target_speed_sqd = agent
2096 .target
2097 .as_ref()
2098 .map(|t| t.target)
2099 .and_then(|e| read_data.velocities.get(e))
2100 .map_or(0.0, |v| v.0.magnitude_squared());
2101 if c.charge_frac() < MIN_CHARGE_FRAC
2102 || (target_speed_sqd > OPTIMAL_TARGET_VELOCITY.powi(2) && c.charge_frac() < 1.0)
2103 {
2104 controller.push_basic_input(InputKind::Primary);
2107 }
2108 }
2110 } else if matches!(self.char_state, CharacterState::RepeaterRanged(c) if self.energy.current() > 5.0 && !matches!(c.stage_section, StageSection::Recover))
2111 {
2112 if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
2115 && line_of_sight_with_target()
2116 {
2117 controller.push_basic_input(InputKind::Secondary);
2119 }
2120 } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2121 if self
2122 .skill_set
2123 .has_skill(Skill::Bow(BowSkill::UnlockShotgun))
2124 && self.energy.current() > 45.0
2125 && rng.gen_bool(0.5)
2126 {
2127 controller.push_basic_input(InputKind::Ability(0));
2129 } else if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
2130 && self.energy.current()
2131 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2132 && !matches!(self.char_state, CharacterState::BasicRanged(c) if !matches!(c.stage_section, StageSection::Recover))
2133 {
2134 controller.push_basic_input(InputKind::Roll);
2137 } else {
2138 self.path_toward_target(
2139 agent,
2140 controller,
2141 tgt_data.pos.0,
2142 read_data,
2143 Path::Separate,
2144 None,
2145 );
2146 if attack_data.angle < 15.0 {
2147 controller.push_basic_input(InputKind::Primary);
2148 }
2149 }
2150 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) && line_of_sight_with_target() {
2151 if self.energy.current() < DESIRED_ENERGY_LEVEL {
2153 controller.push_basic_input(InputKind::Primary);
2155 } else {
2156 controller.push_basic_input(InputKind::Secondary);
2158 }
2159 }
2160 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2163 if let Some((bearing, speed)) = agent.chaser.chase(
2165 &*read_data.terrain,
2166 self.pos.0,
2167 self.vel.0,
2168 tgt_data.pos.0,
2169 TraversalConfig {
2170 min_tgt_dist: 1.25,
2171 ..self.traversal_config
2172 },
2173 ) {
2174 controller.inputs.move_dir =
2175 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2176 }
2177 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2178 if let Some((bearing, speed)) = agent.chaser.chase(
2180 &*read_data.terrain,
2181 self.pos.0,
2182 self.vel.0,
2183 tgt_data.pos.0,
2184 TraversalConfig {
2185 min_tgt_dist: 1.25,
2186 ..self.traversal_config
2187 },
2188 ) {
2189 if line_of_sight_with_target() && attack_data.angle < 45.0 {
2190 controller.inputs.move_dir = bearing
2191 .xy()
2192 .rotated_z(rng.gen_range(0.5..1.57))
2193 .try_normalized()
2194 .unwrap_or_else(Vec2::zero)
2195 * speed;
2196 } else {
2197 controller.inputs.move_dir =
2199 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2200 self.jump_if(bearing.z > 1.5, controller);
2201 controller.inputs.move_z = bearing.z;
2202 }
2203 }
2204 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
2206 && attack_data.dist_sqrd < 16.0f32.powi(2)
2207 && rng.gen::<f32>() < 0.01
2208 {
2209 controller.push_basic_input(InputKind::Roll);
2210 }
2211 } else {
2212 self.path_toward_target(
2214 agent,
2215 controller,
2216 tgt_data.pos.0,
2217 read_data,
2218 Path::Partial,
2219 None,
2220 );
2221 }
2222 }
2223
2224 pub fn handle_staff_attack(
2225 &self,
2226 agent: &mut Agent,
2227 controller: &mut Controller,
2228 attack_data: &AttackData,
2229 tgt_data: &TargetData,
2230 read_data: &ReadData,
2231 rng: &mut impl Rng,
2232 ) {
2233 enum ActionStateConditions {
2234 ConditionStaffCanShockwave = 0,
2235 }
2236 let context = AbilityContext::from(self.stance, Some(self.inventory), self.combo);
2237 let extract_ability = |input: AbilityInput| {
2238 self.active_abilities
2239 .activate_ability(
2240 input,
2241 Some(self.inventory),
2242 self.skill_set,
2243 self.body,
2244 Some(self.char_state),
2245 &context,
2246 self.stats,
2247 )
2248 .map_or(Default::default(), |a| a.0)
2249 };
2250 let (flamethrower, shockwave) = (
2251 extract_ability(AbilityInput::Secondary),
2252 extract_ability(AbilityInput::Auxiliary(0)),
2253 );
2254 let flamethrower_range = match flamethrower {
2255 CharacterAbility::BasicBeam { range, .. } => range,
2256 _ => 20.0_f32,
2257 };
2258 let shockwave_cost = shockwave.energy_cost();
2259 if self.body.is_some_and(|b| b.is_humanoid())
2260 && attack_data.in_min_range()
2261 && self.energy.current()
2262 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2263 && !matches!(self.char_state, CharacterState::Shockwave(_))
2264 {
2265 controller.push_basic_input(InputKind::Roll);
2268 } else if matches!(self.char_state, CharacterState::Shockwave(_)) {
2269 agent.combat_state.conditions
2270 [ActionStateConditions::ConditionStaffCanShockwave as usize] = false;
2271 } else if agent.combat_state.conditions
2272 [ActionStateConditions::ConditionStaffCanShockwave as usize]
2273 && matches!(self.char_state, CharacterState::Wielding(_))
2274 {
2275 controller.push_basic_input(InputKind::Ability(0));
2276 } else if !matches!(self.char_state, CharacterState::Shockwave(c) if !matches!(c.stage_section, StageSection::Recover))
2277 {
2278 let target_approaching_speed = -agent
2280 .target
2281 .as_ref()
2282 .map(|t| t.target)
2283 .and_then(|e| read_data.velocities.get(e))
2284 .map_or(0.0, |v| v.0.dot(self.ori.look_vec()));
2285 if self
2286 .skill_set
2287 .has_skill(Skill::Staff(StaffSkill::UnlockShockwave))
2288 && target_approaching_speed > 12.0
2289 && self.energy.current() > shockwave_cost
2290 {
2291 if matches!(self.char_state, CharacterState::Wielding(_)) {
2293 controller.push_basic_input(InputKind::Ability(0));
2294 } else {
2295 agent.combat_state.conditions
2296 [ActionStateConditions::ConditionStaffCanShockwave as usize] = true;
2297 }
2298 } else if self.energy.current()
2299 > shockwave_cost
2300 + CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2301 && attack_data.dist_sqrd < flamethrower_range.powi(2)
2302 {
2303 controller.push_basic_input(InputKind::Secondary);
2304 } else {
2305 controller.push_basic_input(InputKind::Primary);
2306 }
2307 }
2308 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2311 if let Some((bearing, speed)) = agent.chaser.chase(
2313 &*read_data.terrain,
2314 self.pos.0,
2315 self.vel.0,
2316 tgt_data.pos.0,
2317 TraversalConfig {
2318 min_tgt_dist: 1.25,
2319 ..self.traversal_config
2320 },
2321 ) {
2322 controller.inputs.move_dir =
2323 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2324 }
2325 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2326 if let Some((bearing, speed)) = agent.chaser.chase(
2328 &*read_data.terrain,
2329 self.pos.0,
2330 self.vel.0,
2331 tgt_data.pos.0,
2332 TraversalConfig {
2333 min_tgt_dist: 1.25,
2334 ..self.traversal_config
2335 },
2336 ) {
2337 if entities_have_line_of_sight(
2338 self.pos,
2339 self.body,
2340 self.scale,
2341 tgt_data.pos,
2342 tgt_data.body,
2343 tgt_data.scale,
2344 read_data,
2345 ) && attack_data.angle < 45.0
2346 {
2347 controller.inputs.move_dir = bearing
2348 .xy()
2349 .rotated_z(rng.gen_range(-1.57..-0.5))
2350 .try_normalized()
2351 .unwrap_or_else(Vec2::zero)
2352 * speed;
2353 } else {
2354 controller.inputs.move_dir =
2356 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2357 self.jump_if(bearing.z > 1.5, controller);
2358 controller.inputs.move_z = bearing.z;
2359 }
2360 }
2361 if self.body.is_some_and(|b| b.is_humanoid())
2363 && attack_data.dist_sqrd < 16.0f32.powi(2)
2364 && !matches!(self.char_state, CharacterState::Shockwave(_))
2365 && rng.gen::<f32>() < 0.02
2366 {
2367 controller.push_basic_input(InputKind::Roll);
2368 }
2369 } else {
2370 self.path_toward_target(
2372 agent,
2373 controller,
2374 tgt_data.pos.0,
2375 read_data,
2376 Path::Partial,
2377 None,
2378 );
2379 }
2380 }
2381
2382 pub fn handle_sceptre_attack(
2383 &self,
2384 agent: &mut Agent,
2385 controller: &mut Controller,
2386 attack_data: &AttackData,
2387 tgt_data: &TargetData,
2388 read_data: &ReadData,
2389 rng: &mut impl Rng,
2390 ) {
2391 const DESIRED_ENERGY_LEVEL: f32 = 50.0;
2392 const DESIRED_COMBO_LEVEL: u32 = 8;
2393
2394 let line_of_sight_with_target = || {
2395 entities_have_line_of_sight(
2396 self.pos,
2397 self.body,
2398 self.scale,
2399 tgt_data.pos,
2400 tgt_data.body,
2401 tgt_data.scale,
2402 read_data,
2403 )
2404 };
2405
2406 if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
2408 && line_of_sight_with_target()
2409 {
2410 if self.energy.current() > DESIRED_ENERGY_LEVEL
2413 && read_data
2414 .combos
2415 .get(*self.entity)
2416 .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
2417 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
2418 buff.iter_kind(BuffKind::Regeneration)
2419 .peekable()
2420 .peek()
2421 .is_some()
2422 })
2423 {
2424 controller.push_basic_input(InputKind::Secondary);
2426 } else if self
2427 .skill_set
2428 .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
2429 && self.energy.current() > DESIRED_ENERGY_LEVEL
2430 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
2431 buff.iter_kind(BuffKind::ProtectingWard)
2432 .peekable()
2433 .peek()
2434 .is_some()
2435 })
2436 {
2437 controller.push_basic_input(InputKind::Ability(0));
2440 } else {
2441 controller.push_basic_input(InputKind::Primary);
2444 }
2445 } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2446 if self.body.is_some_and(|b| b.is_humanoid())
2447 && self.energy.current()
2448 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2449 && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
2450 {
2451 controller.push_basic_input(InputKind::Roll);
2454 } else if attack_data.angle < 15.0 {
2455 controller.push_basic_input(InputKind::Primary);
2456 }
2457 }
2458 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2461 if let Some((bearing, speed)) = agent.chaser.chase(
2463 &*read_data.terrain,
2464 self.pos.0,
2465 self.vel.0,
2466 tgt_data.pos.0,
2467 TraversalConfig {
2468 min_tgt_dist: 1.25,
2469 ..self.traversal_config
2470 },
2471 ) {
2472 controller.inputs.move_dir =
2473 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2474 }
2475 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2476 if let Some((bearing, speed)) = agent.chaser.chase(
2478 &*read_data.terrain,
2479 self.pos.0,
2480 self.vel.0,
2481 tgt_data.pos.0,
2482 TraversalConfig {
2483 min_tgt_dist: 1.25,
2484 ..self.traversal_config
2485 },
2486 ) {
2487 if line_of_sight_with_target() && attack_data.angle < 45.0 {
2488 controller.inputs.move_dir = bearing
2489 .xy()
2490 .rotated_z(rng.gen_range(0.5..1.57))
2491 .try_normalized()
2492 .unwrap_or_else(Vec2::zero)
2493 * speed;
2494 } else {
2495 controller.inputs.move_dir =
2497 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2498 self.jump_if(bearing.z > 1.5, controller);
2499 controller.inputs.move_z = bearing.z;
2500 }
2501 }
2502 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
2504 && !matches!(self.char_state, CharacterState::BasicAura(_))
2505 && attack_data.dist_sqrd < 16.0f32.powi(2)
2506 && rng.gen::<f32>() < 0.01
2507 {
2508 controller.push_basic_input(InputKind::Roll);
2509 }
2510 } else {
2511 self.path_toward_target(
2513 agent,
2514 controller,
2515 tgt_data.pos.0,
2516 read_data,
2517 Path::Partial,
2518 None,
2519 );
2520 }
2521 }
2522
2523 pub fn handle_stone_golem_attack(
2524 &self,
2525 agent: &mut Agent,
2526 controller: &mut Controller,
2527 attack_data: &AttackData,
2528 tgt_data: &TargetData,
2529 read_data: &ReadData,
2530 ) {
2531 enum ActionStateTimers {
2532 TimerHandleStoneGolemAttack = 0, }
2534
2535 if attack_data.in_min_range() && attack_data.angle < 90.0 {
2536 controller.inputs.move_dir = Vec2::zero();
2537 controller.push_basic_input(InputKind::Primary);
2538 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2540 if self.vel.0.is_approx_zero() {
2541 controller.push_basic_input(InputKind::Ability(0));
2542 }
2543 if self
2544 .path_toward_target(
2545 agent,
2546 controller,
2547 tgt_data.pos.0,
2548 read_data,
2549 Path::Separate,
2550 None,
2551 )
2552 .is_some()
2553 && entities_have_line_of_sight(
2554 self.pos,
2555 self.body,
2556 self.scale,
2557 tgt_data.pos,
2558 tgt_data.body,
2559 tgt_data.scale,
2560 read_data,
2561 )
2562 && attack_data.angle < 90.0
2563 {
2564 if agent.combat_state.timers
2565 [ActionStateTimers::TimerHandleStoneGolemAttack as usize]
2566 > 5.0
2567 {
2568 controller.push_basic_input(InputKind::Secondary);
2569 agent.combat_state.timers
2570 [ActionStateTimers::TimerHandleStoneGolemAttack as usize] = 0.0;
2571 } else {
2572 agent.combat_state.timers
2573 [ActionStateTimers::TimerHandleStoneGolemAttack as usize] += read_data.dt.0;
2574 }
2575 }
2576 } else {
2577 self.path_toward_target(
2578 agent,
2579 controller,
2580 tgt_data.pos.0,
2581 read_data,
2582 Path::Partial,
2583 None,
2584 );
2585 }
2586 }
2587
2588 pub fn handle_iron_golem_attack(
2589 &self,
2590 agent: &mut Agent,
2591 controller: &mut Controller,
2592 attack_data: &AttackData,
2593 tgt_data: &TargetData,
2594 read_data: &ReadData,
2595 ) {
2596 enum ActionStateTimers {
2597 AttackTimer = 0,
2598 }
2599
2600 let home = agent.patrol_origin.unwrap_or(self.pos.0);
2601
2602 let attack_select =
2603 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
2604 0
2605 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.5 {
2606 1
2607 } else {
2608 2
2609 };
2610 if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
2612 self.path_toward_target(agent, controller, home, read_data, Path::Full, None);
2613 } else if tgt_data.pos.0.z > home.z + 5.0 {
2615 controller.push_basic_input(InputKind::Ability(0))
2616 } else if attack_data.in_min_range() {
2617 controller.inputs.move_dir = Vec2::zero();
2618 controller.push_basic_input(InputKind::Primary);
2619 } else {
2620 match attack_select {
2621 0 => {
2622 controller.push_basic_input(InputKind::Ability(0))
2624 },
2625 1 => {
2626 controller.push_basic_input(InputKind::Ability(1))
2628 },
2629 _ => {
2630 controller.push_basic_input(InputKind::Secondary)
2632 },
2633 };
2634 };
2635 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
2636 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 7.5 {
2637 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
2638 };
2639 }
2640
2641 pub fn handle_circle_charge_attack(
2642 &self,
2643 agent: &mut Agent,
2644 controller: &mut Controller,
2645 attack_data: &AttackData,
2646 tgt_data: &TargetData,
2647 read_data: &ReadData,
2648 radius: u32,
2649 circle_time: u32,
2650 rng: &mut impl Rng,
2651 ) {
2652 enum ActionStateCountersF {
2653 CounterFHandleCircleChargeAttack = 0,
2654 }
2655
2656 enum ActionStateCountersI {
2657 CounterIHandleCircleChargeAttack = 0,
2658 }
2659
2660 if agent.combat_state.counters
2661 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
2662 >= circle_time as f32
2663 {
2664 controller.push_basic_input(InputKind::Secondary);
2666 }
2667 if attack_data.in_min_range() {
2668 if agent.combat_state.counters
2669 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
2670 > 0.0
2671 {
2672 agent.combat_state.counters
2674 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] = 0.0;
2675 agent.combat_state.int_counters
2676 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] = 0;
2677 } else {
2678 controller.push_basic_input(InputKind::Primary);
2680 controller.inputs.move_dir = Vec2::zero();
2681 }
2682 } else if attack_data.dist_sqrd < (radius as f32 + attack_data.min_attack_dist).powi(2) {
2683 if agent.combat_state.int_counters
2685 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize]
2686 == 0
2687 {
2688 agent.combat_state.int_counters
2690 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] =
2691 1 + rng.gen_bool(0.5) as u8;
2692 }
2693 if agent.combat_state.counters
2694 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
2695 < circle_time as f32
2696 {
2697 let move_dir = match agent.combat_state.int_counters
2699 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize]
2700 {
2701 1 =>
2702 {
2704 (tgt_data.pos.0 - self.pos.0)
2705 .xy()
2706 .rotated_z(0.47 * PI)
2707 .try_normalized()
2708 .unwrap_or_else(Vec2::unit_y)
2709 },
2710 2 =>
2711 {
2713 (tgt_data.pos.0 - self.pos.0)
2714 .xy()
2715 .rotated_z(-0.47 * PI)
2716 .try_normalized()
2717 .unwrap_or_else(Vec2::unit_y)
2718 },
2719 _ =>
2720 {
2722 Vec2::zero()
2723 },
2724 };
2725 let obstacle = read_data
2726 .terrain
2727 .ray(
2728 self.pos.0 + Vec3::unit_z(),
2729 self.pos.0 + move_dir.with_z(0.0) * 2.0 + Vec3::unit_z(),
2730 )
2731 .until(Block::is_solid)
2732 .cast()
2733 .1
2734 .map_or(true, |b| b.is_some());
2735 if obstacle {
2736 agent.combat_state.counters
2738 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] =
2739 circle_time as f32;
2740 }
2741 controller.inputs.move_dir = move_dir;
2742 agent.combat_state.counters
2744 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] +=
2745 read_data.dt.0;
2746 }
2747 } else {
2749 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2750 Path::Separate
2752 } else {
2753 Path::Partial
2754 };
2755 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
2756 }
2757 }
2758
2759 pub fn handle_quadlow_ranged_attack(
2760 &self,
2761 agent: &mut Agent,
2762 controller: &mut Controller,
2763 attack_data: &AttackData,
2764 tgt_data: &TargetData,
2765 read_data: &ReadData,
2766 ) {
2767 enum ActionStateTimers {
2768 TimerHandleQuadLowRanged = 0,
2769 }
2770
2771 if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2)
2772 && attack_data.angle < 90.0
2773 {
2774 controller.inputs.move_dir = if !attack_data.in_min_range() {
2775 (tgt_data.pos.0 - self.pos.0)
2776 .xy()
2777 .try_normalized()
2778 .unwrap_or_else(Vec2::unit_y)
2779 } else {
2780 Vec2::zero()
2781 };
2782
2783 controller.push_basic_input(InputKind::Primary);
2784 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2785 if let Some((bearing, speed)) = agent.chaser.chase(
2786 &*read_data.terrain,
2787 self.pos.0,
2788 self.vel.0,
2789 tgt_data.pos.0,
2790 TraversalConfig {
2791 min_tgt_dist: 1.25,
2792 ..self.traversal_config
2793 },
2794 ) {
2795 if attack_data.angle < 15.0
2796 && entities_have_line_of_sight(
2797 self.pos,
2798 self.body,
2799 self.scale,
2800 tgt_data.pos,
2801 tgt_data.body,
2802 tgt_data.scale,
2803 read_data,
2804 )
2805 {
2806 if agent.combat_state.timers
2807 [ActionStateTimers::TimerHandleQuadLowRanged as usize]
2808 > 5.0
2809 {
2810 agent.combat_state.timers
2811 [ActionStateTimers::TimerHandleQuadLowRanged as usize] = 0.0;
2812 } else if agent.combat_state.timers
2813 [ActionStateTimers::TimerHandleQuadLowRanged as usize]
2814 > 2.5
2815 {
2816 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
2817 .xy()
2818 .rotated_z(1.75 * PI)
2819 .try_normalized()
2820 .unwrap_or_else(Vec2::zero)
2821 * speed;
2822 agent.combat_state.timers
2823 [ActionStateTimers::TimerHandleQuadLowRanged as usize] +=
2824 read_data.dt.0;
2825 } else {
2826 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
2827 .xy()
2828 .rotated_z(0.25 * PI)
2829 .try_normalized()
2830 .unwrap_or_else(Vec2::zero)
2831 * speed;
2832 agent.combat_state.timers
2833 [ActionStateTimers::TimerHandleQuadLowRanged as usize] +=
2834 read_data.dt.0;
2835 }
2836 controller.push_basic_input(InputKind::Secondary);
2837 self.jump_if(bearing.z > 1.5, controller);
2838 controller.inputs.move_z = bearing.z;
2839 } else {
2840 controller.inputs.move_dir =
2841 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2842 self.jump_if(bearing.z > 1.5, controller);
2843 controller.inputs.move_z = bearing.z;
2844 }
2845 } else {
2846 agent.target = None;
2847 }
2848 } else {
2849 self.path_toward_target(
2850 agent,
2851 controller,
2852 tgt_data.pos.0,
2853 read_data,
2854 Path::Partial,
2855 None,
2856 );
2857 }
2858 }
2859
2860 pub fn handle_tail_slap_attack(
2861 &self,
2862 agent: &mut Agent,
2863 controller: &mut Controller,
2864 attack_data: &AttackData,
2865 tgt_data: &TargetData,
2866 read_data: &ReadData,
2867 ) {
2868 enum ActionStateTimers {
2869 TimerTailSlap = 0,
2870 }
2871
2872 if attack_data.angle < 90.0
2873 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
2874 {
2875 if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 4.0 {
2876 controller.push_cancel_input(InputKind::Primary);
2877 agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] = 0.0;
2878 } else if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 1.0 {
2879 controller.push_basic_input(InputKind::Primary);
2880 agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] +=
2881 read_data.dt.0;
2882 } else {
2883 controller.push_basic_input(InputKind::Secondary);
2884 agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] +=
2885 read_data.dt.0;
2886 }
2887 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
2888 .xy()
2889 .try_normalized()
2890 .unwrap_or_else(Vec2::unit_y)
2891 * 0.1;
2892 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2893 self.path_toward_target(
2894 agent,
2895 controller,
2896 tgt_data.pos.0,
2897 read_data,
2898 Path::Separate,
2899 None,
2900 );
2901 } else {
2902 self.path_toward_target(
2903 agent,
2904 controller,
2905 tgt_data.pos.0,
2906 read_data,
2907 Path::Partial,
2908 None,
2909 );
2910 }
2911 }
2912
2913 pub fn handle_quadlow_quick_attack(
2914 &self,
2915 agent: &mut Agent,
2916 controller: &mut Controller,
2917 attack_data: &AttackData,
2918 tgt_data: &TargetData,
2919 read_data: &ReadData,
2920 ) {
2921 if attack_data.angle < 90.0
2922 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
2923 {
2924 controller.inputs.move_dir = Vec2::zero();
2925 controller.push_basic_input(InputKind::Secondary);
2926 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2)
2927 && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2)
2928 && attack_data.angle < 90.0
2929 {
2930 controller.push_basic_input(InputKind::Primary);
2931 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
2932 .xy()
2933 .rotated_z(-0.47 * PI)
2934 .try_normalized()
2935 .unwrap_or_else(Vec2::unit_y);
2936 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2937 self.path_toward_target(
2938 agent,
2939 controller,
2940 tgt_data.pos.0,
2941 read_data,
2942 Path::Separate,
2943 None,
2944 );
2945 } else {
2946 self.path_toward_target(
2947 agent,
2948 controller,
2949 tgt_data.pos.0,
2950 read_data,
2951 Path::Partial,
2952 None,
2953 );
2954 }
2955 }
2956
2957 pub fn handle_quadlow_basic_attack(
2958 &self,
2959 agent: &mut Agent,
2960 controller: &mut Controller,
2961 attack_data: &AttackData,
2962 tgt_data: &TargetData,
2963 read_data: &ReadData,
2964 ) {
2965 enum ActionStateTimers {
2966 TimerQuadLowBasic = 0,
2967 }
2968
2969 if attack_data.angle < 70.0
2970 && attack_data.dist_sqrd < (1.3 * attack_data.min_attack_dist).powi(2)
2971 {
2972 controller.inputs.move_dir = Vec2::zero();
2973 if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 5.0 {
2974 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] = 0.0;
2975 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 2.0
2976 {
2977 controller.push_basic_input(InputKind::Secondary);
2978 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] +=
2979 read_data.dt.0;
2980 } else {
2981 controller.push_basic_input(InputKind::Primary);
2982 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] +=
2983 read_data.dt.0;
2984 }
2985 } else {
2986 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2987 Path::Separate
2988 } else {
2989 Path::Partial
2990 };
2991 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
2992 }
2993 }
2994
2995 pub fn handle_quadmed_jump_attack(
2996 &self,
2997 agent: &mut Agent,
2998 controller: &mut Controller,
2999 attack_data: &AttackData,
3000 tgt_data: &TargetData,
3001 read_data: &ReadData,
3002 ) {
3003 if attack_data.angle < 90.0
3004 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
3005 {
3006 controller.inputs.move_dir = Vec2::zero();
3007 controller.push_basic_input(InputKind::Secondary);
3008 } else if attack_data.angle < 15.0
3009 && attack_data.dist_sqrd < (5.0 * attack_data.min_attack_dist).powi(2)
3010 {
3011 controller.push_basic_input(InputKind::Ability(0));
3012 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3013 if self
3014 .path_toward_target(
3015 agent,
3016 controller,
3017 tgt_data.pos.0,
3018 read_data,
3019 Path::Separate,
3020 None,
3021 )
3022 .is_some()
3023 && attack_data.angle < 15.0
3024 && entities_have_line_of_sight(
3025 self.pos,
3026 self.body,
3027 self.scale,
3028 tgt_data.pos,
3029 tgt_data.body,
3030 tgt_data.scale,
3031 read_data,
3032 )
3033 {
3034 controller.push_basic_input(InputKind::Primary);
3035 }
3036 } else {
3037 self.path_toward_target(
3038 agent,
3039 controller,
3040 tgt_data.pos.0,
3041 read_data,
3042 Path::Partial,
3043 None,
3044 );
3045 }
3046 }
3047
3048 pub fn handle_quadmed_basic_attack(
3049 &self,
3050 agent: &mut Agent,
3051 controller: &mut Controller,
3052 attack_data: &AttackData,
3053 tgt_data: &TargetData,
3054 read_data: &ReadData,
3055 ) {
3056 enum ActionStateTimers {
3057 TimerQuadMedBasic = 0,
3058 }
3059
3060 if attack_data.angle < 90.0 && attack_data.in_min_range() {
3061 controller.inputs.move_dir = Vec2::zero();
3062 if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 2.0 {
3063 controller.push_basic_input(InputKind::Secondary);
3064 agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] +=
3065 read_data.dt.0;
3066 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 3.0
3067 {
3068 controller.push_basic_input(InputKind::Primary);
3069 agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] +=
3070 read_data.dt.0;
3071 } else {
3072 agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] = 0.0;
3073 }
3074 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3075 self.path_toward_target(
3076 agent,
3077 controller,
3078 tgt_data.pos.0,
3079 read_data,
3080 Path::Separate,
3081 None,
3082 );
3083 } else {
3084 self.path_toward_target(
3085 agent,
3086 controller,
3087 tgt_data.pos.0,
3088 read_data,
3089 Path::Partial,
3090 None,
3091 );
3092 }
3093 }
3094
3095 pub fn handle_quadmed_hoof_attack(
3096 &self,
3097 agent: &mut Agent,
3098 controller: &mut Controller,
3099 attack_data: &AttackData,
3100 tgt_data: &TargetData,
3101 read_data: &ReadData,
3102 ) {
3103 const HOOF_ATTACK_RANGE: f32 = 1.0;
3104 const HOOF_ATTACK_ANGLE: f32 = 50.0;
3105
3106 if attack_data.angle < HOOF_ATTACK_ANGLE
3107 && attack_data.dist_sqrd
3108 < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.front_radius())).powi(2)
3109 {
3110 controller.inputs.move_dir = Vec2::zero();
3111 controller.push_basic_input(InputKind::Primary);
3112 } else {
3113 self.path_toward_target(
3114 agent,
3115 controller,
3116 tgt_data.pos.0,
3117 read_data,
3118 Path::Full,
3119 None,
3120 );
3121 }
3122 }
3123
3124 pub fn handle_quadlow_beam_attack(
3125 &self,
3126 agent: &mut Agent,
3127 controller: &mut Controller,
3128 attack_data: &AttackData,
3129 tgt_data: &TargetData,
3130 read_data: &ReadData,
3131 ) {
3132 enum ActionStateTimers {
3133 TimerQuadLowBeam = 0,
3134 }
3135 if attack_data.angle < 90.0
3136 && attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2)
3137 {
3138 controller.inputs.move_dir = Vec2::zero();
3139 controller.push_basic_input(InputKind::Secondary);
3140 } else if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2)
3141 && attack_data.angle < 15.0
3142 {
3143 if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 2.0 {
3144 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3145 .xy()
3146 .rotated_z(0.47 * PI)
3147 .try_normalized()
3148 .unwrap_or_else(Vec2::unit_y);
3149 controller.push_basic_input(InputKind::Primary);
3150 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3151 read_data.dt.0;
3152 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 4.0
3153 && attack_data.angle < 15.0
3154 {
3155 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3156 .xy()
3157 .rotated_z(-0.47 * PI)
3158 .try_normalized()
3159 .unwrap_or_else(Vec2::unit_y);
3160 controller.push_basic_input(InputKind::Primary);
3161 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3162 read_data.dt.0;
3163 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 6.0
3164 && attack_data.angle < 15.0
3165 {
3166 controller.push_basic_input(InputKind::Ability(0));
3167 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3168 read_data.dt.0;
3169 } else {
3170 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] = 0.0;
3171 }
3172 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3173 self.path_toward_target(
3174 agent,
3175 controller,
3176 tgt_data.pos.0,
3177 read_data,
3178 Path::Separate,
3179 None,
3180 );
3181 } else {
3182 self.path_toward_target(
3183 agent,
3184 controller,
3185 tgt_data.pos.0,
3186 read_data,
3187 Path::Partial,
3188 None,
3189 );
3190 }
3191 }
3192
3193 pub fn handle_organ_aura_attack(
3194 &self,
3195 agent: &mut Agent,
3196 controller: &mut Controller,
3197 attack_data: &AttackData,
3198 _tgt_data: &TargetData,
3199 read_data: &ReadData,
3200 ) {
3201 enum ActionStateTimers {
3202 TimerOrganAura = 0,
3203 }
3204
3205 const ORGAN_AURA_DURATION: f32 = 34.75;
3206 if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2) {
3207 if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize]
3208 > ORGAN_AURA_DURATION
3209 {
3210 agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] = 0.0;
3211 } else if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] < 1.0 {
3212 controller.push_basic_input(InputKind::Primary);
3213 agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] +=
3214 read_data.dt.0;
3215 } else {
3216 agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] +=
3217 read_data.dt.0;
3218 }
3219 } else {
3220 agent.target = None;
3221 }
3222 }
3223
3224 pub fn handle_theropod_attack(
3225 &self,
3226 agent: &mut Agent,
3227 controller: &mut Controller,
3228 attack_data: &AttackData,
3229 tgt_data: &TargetData,
3230 read_data: &ReadData,
3231 ) {
3232 if attack_data.angle < 90.0 && attack_data.in_min_range() {
3233 controller.inputs.move_dir = Vec2::zero();
3234 controller.push_basic_input(InputKind::Primary);
3235 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3236 self.path_toward_target(
3237 agent,
3238 controller,
3239 tgt_data.pos.0,
3240 read_data,
3241 Path::Separate,
3242 None,
3243 );
3244 } else {
3245 self.path_toward_target(
3246 agent,
3247 controller,
3248 tgt_data.pos.0,
3249 read_data,
3250 Path::Partial,
3251 None,
3252 );
3253 }
3254 }
3255
3256 pub fn handle_turret_attack(
3257 &self,
3258 agent: &mut Agent,
3259 controller: &mut Controller,
3260 attack_data: &AttackData,
3261 tgt_data: &TargetData,
3262 read_data: &ReadData,
3263 ) {
3264 if entities_have_line_of_sight(
3265 self.pos,
3266 self.body,
3267 self.scale,
3268 tgt_data.pos,
3269 tgt_data.body,
3270 tgt_data.scale,
3271 read_data,
3272 ) && attack_data.angle < 15.0
3273 {
3274 controller.push_basic_input(InputKind::Primary);
3275 } else {
3276 agent.target = None;
3277 }
3278 }
3279
3280 pub fn handle_fixed_turret_attack(
3281 &self,
3282 agent: &mut Agent,
3283 controller: &mut Controller,
3284 attack_data: &AttackData,
3285 tgt_data: &TargetData,
3286 read_data: &ReadData,
3287 ) {
3288 controller.inputs.look_dir = self.ori.look_dir();
3289 if entities_have_line_of_sight(
3290 self.pos,
3291 self.body,
3292 self.scale,
3293 tgt_data.pos,
3294 tgt_data.body,
3295 tgt_data.scale,
3296 read_data,
3297 ) && attack_data.angle < 15.0
3298 {
3299 controller.push_basic_input(InputKind::Primary);
3300 } else {
3301 agent.target = None;
3302 }
3303 }
3304
3305 pub fn handle_rotating_turret_attack(
3306 &self,
3307 agent: &mut Agent,
3308 controller: &mut Controller,
3309 tgt_data: &TargetData,
3310 read_data: &ReadData,
3311 ) {
3312 controller.inputs.look_dir = Dir::new(
3313 Quaternion::from_xyzw(self.ori.look_dir().x, self.ori.look_dir().y, 0.0, 0.0)
3314 .rotated_z(6.0 * read_data.dt.0)
3315 .into_vec3()
3316 .try_normalized()
3317 .unwrap_or_default(),
3318 );
3319 if entities_have_line_of_sight(
3320 self.pos,
3321 self.body,
3322 self.scale,
3323 tgt_data.pos,
3324 tgt_data.body,
3325 tgt_data.scale,
3326 read_data,
3327 ) {
3328 controller.push_basic_input(InputKind::Primary);
3329 } else {
3330 agent.target = None;
3331 }
3332 }
3333
3334 pub fn handle_radial_turret_attack(&self, controller: &mut Controller) {
3335 controller.push_basic_input(InputKind::Primary);
3336 }
3337
3338 pub fn handle_fiery_tornado_attack(&self, agent: &mut Agent, controller: &mut Controller) {
3339 enum Conditions {
3340 AuraEmited = 0,
3341 }
3342 if matches!(self.char_state, CharacterState::BasicAura(c) if matches!(c.stage_section, StageSection::Recover))
3343 {
3344 agent.combat_state.conditions[Conditions::AuraEmited as usize] = true;
3345 }
3346 if !agent.combat_state.conditions[Conditions::AuraEmited as usize] {
3348 controller.push_basic_input(InputKind::Secondary);
3349 } else {
3350 controller.push_basic_input(InputKind::Primary);
3352 }
3353 }
3354
3355 pub fn handle_mindflayer_attack(
3356 &self,
3357 agent: &mut Agent,
3358 controller: &mut Controller,
3359 _attack_data: &AttackData,
3360 tgt_data: &TargetData,
3361 read_data: &ReadData,
3362 _rng: &mut impl Rng,
3363 ) {
3364 enum FCounters {
3365 SummonThreshold = 0,
3366 }
3367 enum Timers {
3368 PositionTimer,
3369 AttackTimer1,
3370 AttackTimer2,
3371 }
3372 enum Conditions {
3373 AttackToggle1,
3374 }
3375 const SUMMON_THRESHOLD: f32 = 0.20;
3376 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3377 agent.combat_state.timers[Timers::PositionTimer as usize] += read_data.dt.0;
3378 agent.combat_state.timers[Timers::AttackTimer1 as usize] += read_data.dt.0;
3379 agent.combat_state.timers[Timers::AttackTimer2 as usize] += read_data.dt.0;
3380 if agent.combat_state.timers[Timers::AttackTimer1 as usize] > 10.0 {
3381 agent.combat_state.timers[Timers::AttackTimer1 as usize] = 0.0
3382 }
3383 agent.combat_state.conditions[Conditions::AttackToggle1 as usize] =
3384 agent.combat_state.timers[Timers::AttackTimer1 as usize] < 5.0;
3385 if matches!(self.char_state, CharacterState::Blink(c) if matches!(c.stage_section, StageSection::Recover))
3386 {
3387 agent.combat_state.timers[Timers::AttackTimer2 as usize] = 0.0
3388 }
3389
3390 let position_timer = agent.combat_state.timers[Timers::PositionTimer as usize];
3391 if position_timer > 60.0 {
3392 agent.combat_state.timers[Timers::PositionTimer as usize] = 0.0;
3393 }
3394 let home = agent.patrol_origin.unwrap_or(self.pos.0);
3395 let p = match position_timer as i32 {
3396 0_i32..=6_i32 => 0,
3397 7_i32..=13_i32 => 2,
3398 14_i32..=20_i32 => 3,
3399 21_i32..=27_i32 => 1,
3400 28_i32..=34_i32 => 4,
3401 35_i32..=47_i32 => 5,
3402 _ => 6,
3403 };
3404 let pos = if p > 5 {
3405 tgt_data.pos.0
3406 } else if p > 3 {
3407 home
3408 } else {
3409 Vec3::new(
3410 home.x + (CARDINALS[p].x * 15) as f32,
3411 home.y + (CARDINALS[p].y * 15) as f32,
3412 home.z,
3413 )
3414 };
3415 if !agent.combat_state.initialized {
3416 agent.combat_state.counters[FCounters::SummonThreshold as usize] =
3419 1.0 - SUMMON_THRESHOLD;
3420 agent.combat_state.initialized = true;
3421 }
3422
3423 if position_timer > 55.0
3424 && health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
3425 {
3426 controller.push_basic_input(InputKind::Ability(2));
3428
3429 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
3430 {
3431 agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
3432 SUMMON_THRESHOLD;
3433 }
3434 } else if p > 5 {
3435 if pos.distance_squared(self.pos.0) > 20.0_f32.powi(2) {
3436 controller.push_action(ControlAction::StartInput {
3438 input: InputKind::Ability(0),
3439 target_entity: None,
3440 select_pos: Some(pos),
3441 });
3442 } else {
3443 controller.push_basic_input(InputKind::Ability(4))
3444 }
3445 } else if p > 4 {
3446 self.path_toward_target(
3448 agent,
3449 controller,
3450 tgt_data.pos.0,
3451 read_data,
3452 Path::Partial,
3453 None,
3454 );
3455
3456 if agent.combat_state.conditions[Conditions::AttackToggle1 as usize] {
3457 controller.push_basic_input(InputKind::Primary);
3458 } else {
3459 controller.push_basic_input(InputKind::Ability(1))
3460 }
3461 } else {
3462 if pos.distance_squared(self.pos.0) > 5.0_f32.powi(2) {
3464 controller.push_action(ControlAction::StartInput {
3465 input: InputKind::Ability(0),
3466 target_entity: None,
3467 select_pos: Some(pos),
3468 });
3469 } else if agent.combat_state.timers[Timers::AttackTimer2 as usize] < 4.0 {
3470 controller.push_basic_input(InputKind::Secondary);
3471 } else {
3472 controller.push_basic_input(InputKind::Ability(3))
3473 }
3474 }
3475 }
3476
3477 pub fn handle_forgemaster_attack(
3478 &self,
3479 agent: &mut Agent,
3480 controller: &mut Controller,
3481 attack_data: &AttackData,
3482 tgt_data: &TargetData,
3483 read_data: &ReadData,
3484 ) {
3485 const MELEE_RANGE: f32 = 6.0;
3486 const MID_RANGE: f32 = 25.0;
3487 const SUMMON_THRESHOLD: f32 = 0.2;
3488
3489 enum FCounters {
3490 SummonThreshold = 0,
3491 }
3492 enum Timers {
3493 AttackRand = 0,
3494 }
3495 if agent.combat_state.timers[Timers::AttackRand as usize] > 10.0 {
3496 agent.combat_state.timers[Timers::AttackRand as usize] = 0.0;
3497 }
3498
3499 let line_of_sight_with_target = || {
3500 entities_have_line_of_sight(
3501 self.pos,
3502 self.body,
3503 self.scale,
3504 tgt_data.pos,
3505 tgt_data.body,
3506 tgt_data.scale,
3507 read_data,
3508 )
3509 };
3510 let home = agent.patrol_origin.unwrap_or(self.pos.0);
3511 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3512 if (5f32.powi(2)..100f32.powi(2)).contains(&home.distance_squared(self.pos.0)) {
3515 controller.push_action(ControlAction::StartInput {
3516 input: InputKind::Ability(5),
3517 target_entity: None,
3518 select_pos: Some(home),
3519 });
3520 } else if !agent.combat_state.initialized {
3521 agent.combat_state.counters[FCounters::SummonThreshold as usize] =
3524 1.0 - SUMMON_THRESHOLD;
3525 agent.combat_state.initialized = true;
3526 } else if health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
3527 {
3528 controller.push_basic_input(InputKind::Ability(0));
3530
3531 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
3532 {
3533 agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
3534 SUMMON_THRESHOLD;
3535 }
3536 } else {
3537 if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
3539 if agent.combat_state.timers[Timers::AttackRand as usize] < 3.5 {
3540 controller.push_basic_input(InputKind::Secondary);
3542 } else {
3543 controller.push_basic_input(InputKind::Ability(3));
3545 }
3546 } else if attack_data.dist_sqrd < MID_RANGE.powi(2) && line_of_sight_with_target() {
3549 if agent.combat_state.timers[Timers::AttackRand as usize] > 6.5 {
3550 controller.push_basic_input(InputKind::Ability(1));
3551 } else if agent.combat_state.timers[Timers::AttackRand as usize] > 3.5 {
3552 controller.push_basic_input(InputKind::Ability(3));
3554 } else if agent.combat_state.timers[Timers::AttackRand as usize] > 2.5 {
3555 controller.push_basic_input(InputKind::Primary);
3557 } else {
3558 controller.push_basic_input(InputKind::Ability(2));
3560 }
3561 } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
3564 if agent.combat_state.timers[Timers::AttackRand as usize] > 6.5 {
3565 controller.push_basic_input(InputKind::Ability(4));
3566 } else {
3567 controller.push_basic_input(InputKind::Primary);
3569 }
3570 }
3571 agent.combat_state.timers[Timers::AttackRand as usize] += read_data.dt.0;
3572 }
3573 self.path_toward_target(agent, controller, home, read_data, Path::Full, None);
3574 }
3575
3576 pub fn handle_flamekeeper_attack(
3577 &self,
3578 agent: &mut Agent,
3579 controller: &mut Controller,
3580 attack_data: &AttackData,
3581 tgt_data: &TargetData,
3582 read_data: &ReadData,
3583 ) {
3584 const MELEE_RANGE: f32 = 6.0;
3585 const MID_RANGE: f32 = 25.0;
3586 const SUMMON_THRESHOLD: f32 = 0.2;
3587
3588 enum FCounters {
3589 SummonThreshold = 0,
3590 }
3591 enum Timers {
3592 AttackRand = 0,
3593 }
3594 if agent.combat_state.timers[Timers::AttackRand as usize] > 5.0 {
3595 agent.combat_state.timers[Timers::AttackRand as usize] = 0.0;
3596 }
3597
3598 let line_of_sight_with_target = || {
3599 entities_have_line_of_sight(
3600 self.pos,
3601 self.body,
3602 self.scale,
3603 tgt_data.pos,
3604 tgt_data.body,
3605 tgt_data.scale,
3606 read_data,
3607 )
3608 };
3609 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3610 if !agent.combat_state.initialized {
3613 agent.combat_state.counters[FCounters::SummonThreshold as usize] =
3614 1.0 - SUMMON_THRESHOLD;
3615 agent.combat_state.initialized = true;
3616 } else if health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
3617 {
3618 controller.push_basic_input(InputKind::Ability(0));
3620 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
3621 {
3622 agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
3623 SUMMON_THRESHOLD;
3624 }
3625 } else {
3626 if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
3628 if agent.combat_state.timers[Timers::AttackRand as usize] < 3.5 {
3629 controller.push_basic_input(InputKind::Secondary);
3631 } else {
3632 controller.push_basic_input(InputKind::Ability(2));
3634 }
3635 } else if attack_data.dist_sqrd < MID_RANGE.powi(2) && line_of_sight_with_target() {
3637 if agent.combat_state.timers[Timers::AttackRand as usize] > 3.5 {
3638 controller.push_basic_input(InputKind::Ability(2));
3640 } else if agent.combat_state.timers[Timers::AttackRand as usize] > 2.5 {
3641 controller.push_basic_input(InputKind::Ability(3));
3643 } else {
3644 controller.push_basic_input(InputKind::Ability(1));
3646 }
3647 } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
3649 controller.push_basic_input(InputKind::Primary);
3651 }
3652 self.path_toward_target(
3653 agent,
3654 controller,
3655 tgt_data.pos.0,
3656 read_data,
3657 Path::Partial,
3658 None,
3659 );
3660 agent.combat_state.timers[Timers::AttackRand as usize] += read_data.dt.0;
3661 }
3662 }
3663
3664 pub fn handle_birdlarge_fire_attack(
3665 &self,
3666 agent: &mut Agent,
3667 controller: &mut Controller,
3668 attack_data: &AttackData,
3669 tgt_data: &TargetData,
3670 read_data: &ReadData,
3671 _rng: &mut impl Rng,
3672 ) {
3673 const PHOENIX_HEAL_THRESHOLD: f32 = 0.20;
3674
3675 enum Conditions {
3676 Healed = 0,
3677 }
3678 enum ActionStateTimers {
3679 AttackTimer1,
3680 AttackTimer2,
3681 WaterTimer,
3682 }
3683
3684 let attack_timer_1 =
3685 if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 2.0 {
3686 0
3687 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 4.0 {
3688 1
3689 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 6.0 {
3690 2
3691 } else {
3692 3
3693 };
3694 agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] += read_data.dt.0;
3695 if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] > 8.0 {
3696 agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] = 0.0;
3698 }
3699 let (attack_timer_2, speed) =
3700 if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 3.0 {
3701 (0, 2.0)
3703 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 6.0 {
3704 (1, 2.0)
3706 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 9.0 {
3707 (0, 3.0)
3709 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 16.0 {
3710 (2, 1.0)
3712 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 20.0 {
3713 (5, 20.0)
3715 } else {
3716 (3, 1.0)
3718 };
3719 agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] += read_data.dt.0;
3720 if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] > 28.0 {
3721 agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] = 0.0;
3723 }
3724 let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
3726 .try_normalized()
3727 .unwrap_or_else(Vec3::zero);
3728 controller.inputs.move_dir = dir_to_target.xy() * speed;
3729
3730 controller.push_basic_input(InputKind::Fly);
3732 if matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { .. })) {
3738 agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] = 2.0;
3739 };
3740 if agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] > 0.0 {
3741 agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] -= read_data.dt.0;
3742 if agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] > 1.0 {
3743 controller.inputs.move_z = 1.0
3744 } else {
3745 controller.push_basic_input(InputKind::Ability(3))
3747 }
3748 } else if self.physics_state.on_ground.is_some() {
3749 controller.push_basic_input(InputKind::Jump);
3750 } else {
3751 let mut maintain_altitude = |set_point| {
3754 let alt = read_data
3755 .terrain
3756 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
3757 .until(Block::is_solid)
3758 .cast()
3759 .0;
3760 let error = set_point - alt;
3761 controller.inputs.move_z = error;
3762 };
3763 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3765 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
3766 {
3767 agent.combat_state.conditions[Conditions::Healed as usize] = true;
3768 }
3769 if !agent.combat_state.conditions[Conditions::Healed as usize]
3770 && PHOENIX_HEAL_THRESHOLD > health_fraction
3771 {
3772 controller.push_basic_input(InputKind::Ability(4));
3773 } else if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (35.0_f32).powi(2) {
3774 maintain_altitude(2.0);
3776 controller.push_basic_input(InputKind::Ability(3))
3777 } else {
3778 match attack_timer_2 {
3779 0 => maintain_altitude(3.0),
3780 1 => {
3781 controller.push_basic_input(InputKind::Ability(1));
3783 },
3784 2 => {
3785 controller.push_basic_input(InputKind::Ability(2));
3787 },
3788 3 => {
3789 if attack_data.dist_sqrd < 4.0_f32.powi(2) && attack_data.angle < 150.0 {
3790 match attack_timer_1 {
3792 1 => {
3793 controller.push_basic_input(InputKind::Primary);
3795 },
3796 3 => {
3797 controller.push_basic_input(InputKind::Secondary)
3799 },
3800 _ => {
3801 controller.push_basic_input(InputKind::Ability(0))
3803 },
3804 }
3805 } else {
3806 match attack_timer_1 {
3807 0 | 2 => {
3808 maintain_altitude(2.0);
3809 },
3810 _ => {
3811 controller.push_basic_input(InputKind::Ability(3))
3813 },
3814 }
3815 }
3816 },
3817 _ => {
3818 maintain_altitude(2.0);
3819 },
3820 }
3821 }
3822 }
3823 }
3824
3825 pub fn handle_wyvern_attack(
3826 &self,
3827 agent: &mut Agent,
3828 controller: &mut Controller,
3829 attack_data: &AttackData,
3830 tgt_data: &TargetData,
3831 read_data: &ReadData,
3832 _rng: &mut impl Rng,
3833 ) {
3834 enum ActionStateTimers {
3835 AttackTimer = 0,
3836 }
3837 controller.push_cancel_input(InputKind::Fly);
3839 if attack_data.dist_sqrd > 30.0_f32.powi(2) {
3840 if entities_have_line_of_sight(
3841 self.pos,
3842 self.body,
3843 self.scale,
3844 tgt_data.pos,
3845 tgt_data.body,
3846 tgt_data.scale,
3847 read_data,
3848 ) && attack_data.angle < 15.0
3849 {
3850 controller.push_basic_input(InputKind::Primary);
3851 }
3852 if let Some((bearing, speed)) = agent.chaser.chase(
3853 &*read_data.terrain,
3854 self.pos.0,
3855 self.vel.0,
3856 tgt_data.pos.0,
3857 TraversalConfig {
3858 min_tgt_dist: 1.25,
3859 ..self.traversal_config
3860 },
3861 ) {
3862 controller.inputs.move_dir =
3863 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
3864 if (self.pos.0.z - tgt_data.pos.0.z) < 35.0 {
3865 controller.push_basic_input(InputKind::Fly);
3866 controller.inputs.move_z = 0.2;
3867 }
3868 }
3869 } else if !read_data
3870 .terrain
3871 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0))
3872 .until(Block::is_solid)
3873 .cast()
3874 .1
3875 .map_or(true, |b| b.is_some())
3876 {
3877 controller.push_basic_input(InputKind::Fly);
3881 let move_dir = tgt_data.pos.0 - self.pos.0;
3882 controller.inputs.move_dir =
3883 move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0;
3884 controller.inputs.move_z = move_dir.z - 0.5;
3885 if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
3886 && attack_data.angle < 15.0
3887 {
3888 controller.push_basic_input(InputKind::Primary);
3889 }
3890 } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) {
3891 self.path_toward_target(
3892 agent,
3893 controller,
3894 tgt_data.pos.0,
3895 read_data,
3896 Path::Separate,
3897 None,
3898 );
3899 } else if attack_data.angle < 15.0 {
3900 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
3901 controller.push_basic_input(InputKind::Ability(1));
3903 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0 {
3904 controller.push_basic_input(InputKind::Ability(0));
3906 } else {
3907 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
3908 }
3909 self.path_toward_target(
3911 agent,
3912 controller,
3913 tgt_data.pos.0,
3914 read_data,
3915 Path::Separate,
3916 Some(0.5),
3917 );
3918 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
3919 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0
3920 && attack_data.angle < 90.0
3921 && attack_data.in_min_range()
3922 {
3923 controller.push_basic_input(InputKind::Secondary);
3925 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
3926 } else {
3927 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
3929 self.path_toward_target(
3931 agent,
3932 controller,
3933 tgt_data.pos.0,
3934 read_data,
3935 Path::Separate,
3936 None,
3937 );
3938 }
3939 }
3940
3941 pub fn handle_birdlarge_breathe_attack(
3942 &self,
3943 agent: &mut Agent,
3944 controller: &mut Controller,
3945 attack_data: &AttackData,
3946 tgt_data: &TargetData,
3947 read_data: &ReadData,
3948 rng: &mut impl Rng,
3949 ) {
3950 enum ActionStateTimers {
3951 TimerBirdLargeBreathe = 0,
3952 }
3953
3954 controller.push_cancel_input(InputKind::Fly);
3956 if attack_data.dist_sqrd > 30.0_f32.powi(2) {
3957 if rng.gen_bool(0.05)
3958 && entities_have_line_of_sight(
3959 self.pos,
3960 self.body,
3961 self.scale,
3962 tgt_data.pos,
3963 tgt_data.body,
3964 tgt_data.scale,
3965 read_data,
3966 )
3967 && attack_data.angle < 15.0
3968 {
3969 controller.push_basic_input(InputKind::Primary);
3970 }
3971 if let Some((bearing, speed)) = agent.chaser.chase(
3972 &*read_data.terrain,
3973 self.pos.0,
3974 self.vel.0,
3975 tgt_data.pos.0,
3976 TraversalConfig {
3977 min_tgt_dist: 1.25,
3978 ..self.traversal_config
3979 },
3980 ) {
3981 controller.inputs.move_dir =
3982 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
3983 if (self.pos.0.z - tgt_data.pos.0.z) < 20.0 {
3984 controller.push_basic_input(InputKind::Fly);
3985 controller.inputs.move_z = 1.0;
3986 }
3987 }
3988 } else if !read_data
3989 .terrain
3990 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0))
3991 .until(Block::is_solid)
3992 .cast()
3993 .1
3994 .map_or(true, |b| b.is_some())
3995 {
3996 controller.push_basic_input(InputKind::Fly);
4000 let move_dir = tgt_data.pos.0 - self.pos.0;
4001 controller.inputs.move_dir =
4002 move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0;
4003 controller.inputs.move_z = move_dir.z - 0.5;
4004 if rng.gen_bool(0.05)
4005 && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4006 && attack_data.angle < 15.0
4007 {
4008 controller.push_basic_input(InputKind::Primary);
4009 }
4010 } else if rng.gen_bool(0.05)
4011 && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4012 && attack_data.angle < 15.0
4013 {
4014 controller.push_basic_input(InputKind::Primary);
4015 } else if rng.gen_bool(0.5)
4016 && (self.pos.0.z - tgt_data.pos.0.z) < 15.0
4017 && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4018 {
4019 controller.push_basic_input(InputKind::Fly);
4020 controller.inputs.move_z = 1.0;
4021 } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) {
4022 self.path_toward_target(
4023 agent,
4024 controller,
4025 tgt_data.pos.0,
4026 read_data,
4027 Path::Separate,
4028 None,
4029 );
4030 } else if self.energy.current() > 60.0
4031 && agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 3.0
4032 && attack_data.angle < 15.0
4033 {
4034 controller.push_basic_input(InputKind::Ability(0));
4036 self.path_toward_target(
4038 agent,
4039 controller,
4040 tgt_data.pos.0,
4041 read_data,
4042 Path::Separate,
4043 Some(0.5),
4044 );
4045 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] +=
4046 read_data.dt.0;
4047 } else if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 6.0
4048 && attack_data.angle < 90.0
4049 && attack_data.in_min_range()
4050 {
4051 controller.push_basic_input(InputKind::Secondary);
4053 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] +=
4054 read_data.dt.0;
4055 } else {
4056 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] = 0.0;
4058 self.path_toward_target(
4060 agent,
4061 controller,
4062 tgt_data.pos.0,
4063 read_data,
4064 Path::Separate,
4065 None,
4066 );
4067 }
4068 }
4069
4070 pub fn handle_birdlarge_basic_attack(
4071 &self,
4072 agent: &mut Agent,
4073 controller: &mut Controller,
4074 attack_data: &AttackData,
4075 tgt_data: &TargetData,
4076 read_data: &ReadData,
4077 ) {
4078 enum ActionStateTimers {
4079 TimerBirdLargeBasic = 0,
4080 }
4081
4082 enum ActionStateConditions {
4083 ConditionBirdLargeBasic = 0, }
4086
4087 const BIRD_ATTACK_RANGE: f32 = 4.0;
4088 const BIRD_CHARGE_DISTANCE: f32 = 15.0;
4089 let bird_attack_distance = self.body.map_or(0.0, |b| b.max_radius()) + BIRD_ATTACK_RANGE;
4090 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] +=
4092 read_data.dt.0;
4093 if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] > 8.0 {
4094 controller.push_basic_input(InputKind::Secondary);
4096 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
4097 {
4098 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] = 0.0;
4100 }
4101 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4102 {
4103 controller.push_basic_input(InputKind::Ability(0));
4105 } else if matches!(self.char_state, CharacterState::ComboMelee2(c) if matches!(c.stage_section, StageSection::Recover))
4106 {
4107 controller.push_basic_input(InputKind::Primary);
4109 } else if attack_data.dist_sqrd > BIRD_CHARGE_DISTANCE.powi(2) {
4110 if attack_data.angle < 60.0 {
4112 controller.push_basic_input(InputKind::Ability(0));
4113 }
4114 } else if attack_data.dist_sqrd < bird_attack_distance.powi(2) {
4115 controller.push_basic_input(InputKind::Primary);
4117 agent.combat_state.conditions
4118 [ActionStateConditions::ConditionBirdLargeBasic as usize] = true;
4119 }
4120 self.path_toward_target(
4122 agent,
4123 controller,
4124 tgt_data.pos.0,
4125 read_data,
4126 Path::Separate,
4127 None,
4128 );
4129 }
4130
4131 pub fn handle_arthropod_ranged_attack(
4132 &self,
4133 agent: &mut Agent,
4134 controller: &mut Controller,
4135 attack_data: &AttackData,
4136 tgt_data: &TargetData,
4137 read_data: &ReadData,
4138 ) {
4139 enum ActionStateTimers {
4140 TimerArthropodRanged = 0,
4141 }
4142
4143 agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] +=
4144 read_data.dt.0;
4145 if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] > 6.0
4146 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
4147 {
4148 controller.inputs.move_dir = Vec2::zero();
4149 controller.push_basic_input(InputKind::Secondary);
4150 if matches!(self.char_state,
4152 CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. })
4153 | CharacterState::SelfBuff(self_buff::Data { stage_section, .. })
4154 if matches!(stage_section, StageSection::Recover))
4155 {
4156 agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] = 0.0;
4157 }
4158 } else if attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2)
4159 && attack_data.angle < 90.0
4160 {
4161 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4162 .xy()
4163 .try_normalized()
4164 .unwrap_or_else(Vec2::unit_y)
4165 * if attack_data.in_min_range() { 0.3 } else { 1.0 };
4167 controller.push_basic_input(InputKind::Primary);
4168 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
4169 if let Some((bearing, speed)) = agent.chaser.chase(
4170 &*read_data.terrain,
4171 self.pos.0,
4172 self.vel.0,
4173 tgt_data.pos.0,
4174 TraversalConfig {
4175 min_tgt_dist: 1.25,
4176 ..self.traversal_config
4177 },
4178 ) {
4179 if attack_data.angle < 15.0
4180 && entities_have_line_of_sight(
4181 self.pos,
4182 self.body,
4183 self.scale,
4184 tgt_data.pos,
4185 tgt_data.body,
4186 tgt_data.scale,
4187 read_data,
4188 )
4189 {
4190 if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize]
4191 > 5.0
4192 {
4193 agent.combat_state.timers
4194 [ActionStateTimers::TimerArthropodRanged as usize] = 0.0;
4195 } else if agent.combat_state.timers
4196 [ActionStateTimers::TimerArthropodRanged as usize]
4197 > 2.5
4198 {
4199 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4200 .xy()
4201 .rotated_z(1.75 * PI)
4202 .try_normalized()
4203 .unwrap_or_else(Vec2::zero)
4204 * speed;
4205 agent.combat_state.timers
4206 [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0;
4207 } else {
4208 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4209 .xy()
4210 .rotated_z(0.25 * PI)
4211 .try_normalized()
4212 .unwrap_or_else(Vec2::zero)
4213 * speed;
4214 agent.combat_state.timers
4215 [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0;
4216 }
4217 controller.push_basic_input(InputKind::Ability(0));
4218 self.jump_if(bearing.z > 1.5, controller);
4219 controller.inputs.move_z = bearing.z;
4220 } else {
4221 controller.inputs.move_dir =
4222 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
4223 self.jump_if(bearing.z > 1.5, controller);
4224 controller.inputs.move_z = bearing.z;
4225 }
4226 } else {
4227 agent.target = None;
4228 }
4229 } else {
4230 self.path_toward_target(
4231 agent,
4232 controller,
4233 tgt_data.pos.0,
4234 read_data,
4235 Path::Partial,
4236 None,
4237 );
4238 }
4239 }
4240
4241 pub fn handle_arthropod_ambush_attack(
4242 &self,
4243 agent: &mut Agent,
4244 controller: &mut Controller,
4245 attack_data: &AttackData,
4246 tgt_data: &TargetData,
4247 read_data: &ReadData,
4248 rng: &mut impl Rng,
4249 ) {
4250 enum ActionStateTimers {
4251 TimersArthropodAmbush = 0,
4252 }
4253
4254 agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] +=
4255 read_data.dt.0;
4256 if agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] > 12.0
4257 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
4258 {
4259 controller.inputs.move_dir = Vec2::zero();
4260 controller.push_basic_input(InputKind::Secondary);
4261 if matches!(self.char_state,
4263 CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. })
4264 | CharacterState::SelfBuff(self_buff::Data { stage_section, .. })
4265 if matches!(stage_section, StageSection::Recover))
4266 {
4267 agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] = 0.0;
4268 }
4269 } else if attack_data.angle < 90.0
4270 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
4271 {
4272 controller.inputs.move_dir = Vec2::zero();
4273 controller.push_basic_input(InputKind::Primary);
4274 } else if rng.gen_bool(0.01)
4275 && attack_data.angle < 60.0
4276 && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2)
4277 {
4278 controller.push_basic_input(InputKind::Ability(0));
4279 } else {
4280 self.path_toward_target(
4281 agent,
4282 controller,
4283 tgt_data.pos.0,
4284 read_data,
4285 Path::Partial,
4286 None,
4287 );
4288 }
4289 }
4290
4291 pub fn handle_arthropod_melee_attack(
4292 &self,
4293 agent: &mut Agent,
4294 controller: &mut Controller,
4295 attack_data: &AttackData,
4296 tgt_data: &TargetData,
4297 read_data: &ReadData,
4298 ) {
4299 enum ActionStateTimers {
4300 TimersArthropodMelee = 0,
4301 }
4302 agent.combat_state.timers[ActionStateTimers::TimersArthropodMelee as usize] +=
4303 read_data.dt.0;
4304 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4305 {
4306 controller.push_basic_input(InputKind::Secondary);
4308 } else if attack_data.dist_sqrd > (2.5 * attack_data.min_attack_dist).powi(2) {
4309 if attack_data.angle < 60.0 {
4311 controller.push_basic_input(InputKind::Secondary);
4312 }
4313 } else if attack_data.angle < 90.0
4314 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
4315 {
4316 controller.inputs.move_dir = Vec2::zero();
4317 controller.push_basic_input(InputKind::Primary);
4318 } else {
4319 self.path_toward_target(
4320 agent,
4321 controller,
4322 tgt_data.pos.0,
4323 read_data,
4324 Path::Partial,
4325 None,
4326 );
4327 }
4328 }
4329
4330 pub fn handle_minotaur_attack(
4331 &self,
4332 agent: &mut Agent,
4333 controller: &mut Controller,
4334 attack_data: &AttackData,
4335 tgt_data: &TargetData,
4336 read_data: &ReadData,
4337 ) {
4338 const MINOTAUR_FRENZY_THRESHOLD: f32 = 0.5;
4339 const MINOTAUR_ATTACK_RANGE: f32 = 5.0;
4340 const MINOTAUR_CHARGE_DISTANCE: f32 = 15.0;
4341
4342 enum ActionStateFCounters {
4343 FCounterMinotaurAttack = 0,
4344 }
4345
4346 enum ActionStateConditions {
4347 ConditionJustCrippledOrCleaved = 0,
4348 }
4349
4350 enum Conditions {
4351 AttackToggle,
4352 }
4353
4354 enum Timers {
4355 CheeseTimer,
4356 CanSeeTarget,
4357 Reposition,
4358 }
4359
4360 let minotaur_attack_distance =
4361 self.body.map_or(0.0, |b| b.max_radius()) + MINOTAUR_ATTACK_RANGE;
4362 let health_fraction = self.health.map_or(1.0, |h| h.fraction());
4363 let home = agent.patrol_origin.unwrap_or(self.pos.0);
4364 let center = Vec2::new(home.x + 50.0, home.y + 75.0);
4365 let cheesed_from_above = tgt_data.pos.0.z > self.pos.0.z + 4.0;
4366 let center_cheesed = (center - self.pos.0.xy()).magnitude_squared() < 16.0_f32.powi(2);
4367 let pillar_cheesed = (center - tgt_data.pos.0.xy()).magnitude_squared() < 16.0_f32.powi(2);
4368 let cheesed = (pillar_cheesed || center_cheesed)
4369 && agent.combat_state.timers[Timers::CheeseTimer as usize] > 4.0;
4370 agent.combat_state.timers[Timers::CheeseTimer as usize] += read_data.dt.0;
4371 agent.combat_state.timers[Timers::CanSeeTarget as usize] += read_data.dt.0;
4372 agent.combat_state.timers[Timers::Reposition as usize] += read_data.dt.0;
4373 if agent.combat_state.timers[Timers::Reposition as usize] > 20.0 {
4374 agent.combat_state.timers[Timers::Reposition as usize] = 0.0;
4375 }
4376 let line_of_sight_with_target = || {
4377 entities_have_line_of_sight(
4378 self.pos,
4379 self.body,
4380 self.scale,
4381 tgt_data.pos,
4382 tgt_data.body,
4383 tgt_data.scale,
4384 read_data,
4385 )
4386 };
4387 if !line_of_sight_with_target() {
4388 agent.combat_state.timers[Timers::CanSeeTarget as usize] = 0.0;
4389 };
4390 let remote_spikes_action = || ControlAction::StartInput {
4391 input: InputKind::Ability(3),
4392 target_entity: None,
4393 select_pos: Some(tgt_data.pos.0),
4394 };
4395 if agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4397 < MINOTAUR_FRENZY_THRESHOLD
4398 && health_fraction > MINOTAUR_FRENZY_THRESHOLD
4399 {
4400 agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize] =
4401 MINOTAUR_FRENZY_THRESHOLD;
4402 }
4403 if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
4404 {
4405 agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
4406 }
4407 if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
4408 {
4409 agent.combat_state.conditions[Conditions::AttackToggle as usize] = false;
4410 if agent.combat_state.timers[Timers::CheeseTimer as usize] > 10.0 {
4411 agent.combat_state.timers[Timers::CheeseTimer as usize] = 0.0;
4412 }
4413 }
4414 if cheesed_from_above || cheesed {
4416 if agent.combat_state.conditions[Conditions::AttackToggle as usize] {
4417 controller.push_basic_input(InputKind::Ability(2));
4418 } else {
4419 controller.push_action(remote_spikes_action());
4420 }
4421 if center_cheesed {
4423 let dir_index = match agent.combat_state.timers[Timers::Reposition as usize] as i32
4425 {
4426 0_i32..5_i32 => 0,
4427 5_i32..10_i32 => 1,
4428 10_i32..15_i32 => 2,
4429 _ => 3,
4430 };
4431 let goto = Vec3::new(
4432 center.x + (CARDINALS[dir_index].x * 25) as f32,
4433 center.y + (CARDINALS[dir_index].y * 25) as f32,
4434 tgt_data.pos.0.z,
4435 );
4436 self.path_toward_target(
4437 agent,
4438 controller,
4439 goto,
4440 read_data,
4441 Path::Partial,
4442 (attack_data.dist_sqrd
4443 < (attack_data.min_attack_dist + MINOTAUR_ATTACK_RANGE / 3.0).powi(2))
4444 .then_some(0.1),
4445 );
4446 }
4447 } else if health_fraction
4448 < agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4449 {
4450 controller.push_basic_input(InputKind::Ability(1));
4452 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4453 {
4454 agent.combat_state.counters
4455 [ActionStateFCounters::FCounterMinotaurAttack as usize] = 0.0;
4456 }
4457 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4458 {
4459 controller.push_basic_input(InputKind::Ability(0));
4461 } else if matches!(self.char_state, CharacterState::ChargedMelee(c) if matches!(c.stage_section, StageSection::Charge) && c.timer < c.static_data.charge_duration)
4462 {
4463 controller.push_basic_input(InputKind::Primary);
4465 } else if attack_data.dist_sqrd > MINOTAUR_CHARGE_DISTANCE.powi(2) {
4466 if attack_data.angle < 60.0 {
4468 controller.push_basic_input(InputKind::Ability(0));
4469 }
4470 } else if attack_data.dist_sqrd < minotaur_attack_distance.powi(2) {
4471 if agent.combat_state.conditions
4472 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize]
4473 && !self.char_state.is_attack()
4474 {
4475 controller.push_basic_input(InputKind::Secondary);
4477 agent.combat_state.conditions
4478 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = false;
4479 } else if !self.char_state.is_attack() {
4480 controller.push_basic_input(InputKind::Primary);
4482 agent.combat_state.conditions
4483 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = true;
4484 }
4485 }
4486 if cheesed_from_above {
4488 self.path_toward_target(agent, controller, home, read_data, Path::Full, None);
4489 } else if agent.combat_state.timers[Timers::CanSeeTarget as usize] > 2.0
4491 || (3.0..18.0).contains(&(self.pos.0.y - home.y))
4493 {
4494 self.path_toward_target(
4495 agent,
4496 controller,
4497 tgt_data.pos.0,
4498 read_data,
4499 Path::Partial,
4500 (attack_data.dist_sqrd
4501 < (attack_data.min_attack_dist + MINOTAUR_ATTACK_RANGE / 3.0).powi(2))
4502 .then_some(0.1),
4503 );
4504 }
4505 }
4506
4507 pub fn handle_cyclops_attack(
4508 &self,
4509 agent: &mut Agent,
4510 controller: &mut Controller,
4511 attack_data: &AttackData,
4512 tgt_data: &TargetData,
4513 read_data: &ReadData,
4514 ) {
4515 const CYCLOPS_MELEE_RANGE: f32 = 9.0;
4517 const CYCLOPS_FIRE_RANGE: f32 = 30.0;
4519 const CYCLOPS_CHARGE_RANGE: f32 = 18.0;
4521 const SHOCKWAVE_THRESHOLD: f32 = 0.6;
4523
4524 enum FCounters {
4525 ShockwaveThreshold = 0,
4526 }
4527 enum Timers {
4528 AttackChange = 0,
4529 }
4530
4531 if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
4532 agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
4533 }
4534
4535 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4536 if !agent.combat_state.initialized {
4539 agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] =
4540 1.0 - SHOCKWAVE_THRESHOLD;
4541 agent.combat_state.initialized = true;
4542 } else if health_fraction
4543 < agent.combat_state.counters[FCounters::ShockwaveThreshold as usize]
4544 {
4545 controller.push_basic_input(InputKind::Ability(2));
4547
4548 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4549 {
4550 agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] -=
4551 SHOCKWAVE_THRESHOLD;
4552 }
4553 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4554 {
4555 controller.push_basic_input(InputKind::Ability(0));
4557 } else if attack_data.dist_sqrd > CYCLOPS_FIRE_RANGE.powi(2) {
4558 controller.push_basic_input(InputKind::Ability(1));
4560 } else if attack_data.dist_sqrd > CYCLOPS_CHARGE_RANGE.powi(2) {
4561 controller.push_basic_input(InputKind::Secondary);
4563 } else if attack_data.dist_sqrd < CYCLOPS_MELEE_RANGE.powi(2) {
4564 if attack_data.angle < 60.0 {
4565 controller.push_basic_input(InputKind::Primary);
4567 } else if attack_data.angle > 60.0 {
4568 controller.push_basic_input(InputKind::Ability(0));
4570 }
4571 }
4572
4573 self.path_toward_target(
4575 agent,
4576 controller,
4577 tgt_data.pos.0,
4578 read_data,
4579 Path::Partial,
4580 (attack_data.dist_sqrd
4581 < (attack_data.min_attack_dist + CYCLOPS_MELEE_RANGE / 2.0).powi(2))
4582 .then_some(0.1),
4583 );
4584 }
4585
4586 pub fn handle_dullahan_attack(
4587 &self,
4588 agent: &mut Agent,
4589 controller: &mut Controller,
4590 attack_data: &AttackData,
4591 tgt_data: &TargetData,
4592 read_data: &ReadData,
4593 ) {
4594 const MELEE_RANGE: f32 = 9.0;
4596 const LONG_RANGE: f32 = 30.0;
4598 const HP_THRESHOLD: f32 = 0.1;
4600 const MID_RANGE: f32 = 18.0;
4602
4603 enum FCounters {
4604 HealthThreshold = 0,
4605 }
4606 enum Timers {
4607 AttackChange = 0,
4608 }
4609 if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
4610 agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
4611 }
4612
4613 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4614 if !agent.combat_state.initialized {
4617 agent.combat_state.counters[FCounters::HealthThreshold as usize] = 1.0 - HP_THRESHOLD;
4618 agent.combat_state.initialized = true;
4619 } else if health_fraction < agent.combat_state.counters[FCounters::HealthThreshold as usize]
4620 {
4621 controller.push_basic_input(InputKind::Ability(0));
4623
4624 if matches!(
4625 self.char_state.ability_info().map(|ai| ai.input),
4626 Some(InputKind::Ability(0))
4627 ) && matches!(self.char_state.stage_section(), Some(StageSection::Recover))
4628 {
4629 agent.combat_state.counters[FCounters::HealthThreshold as usize] -= HP_THRESHOLD;
4630 }
4631 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4632 {
4633 controller.push_basic_input(InputKind::Ability(0));
4635 } else if attack_data.dist_sqrd > LONG_RANGE.powi(2) {
4636 controller.push_basic_input(InputKind::Ability(1));
4638 } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
4639 controller.push_basic_input(InputKind::Secondary);
4641 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4642 if attack_data.angle < 60.0 {
4643 controller.push_basic_input(InputKind::Primary);
4645 } else if attack_data.angle > 60.0 {
4646 controller.push_basic_input(InputKind::Ability(0));
4648 }
4649 }
4650
4651 self.path_toward_target(
4653 agent,
4654 controller,
4655 tgt_data.pos.0,
4656 read_data,
4657 Path::Full,
4658 (attack_data.dist_sqrd < (attack_data.min_attack_dist + MELEE_RANGE / 2.0).powi(2))
4659 .then_some(0.1),
4660 );
4661 }
4662
4663 pub fn handle_grave_warden_attack(
4664 &self,
4665 agent: &mut Agent,
4666 controller: &mut Controller,
4667 attack_data: &AttackData,
4668 tgt_data: &TargetData,
4669 read_data: &ReadData,
4670 ) {
4671 const GOLEM_MELEE_RANGE: f32 = 4.0;
4672 const GOLEM_LASER_RANGE: f32 = 30.0;
4673 const GOLEM_LONG_RANGE: f32 = 50.0;
4674 const GOLEM_TARGET_SPEED: f32 = 8.0;
4675
4676 enum ActionStateFCounters {
4677 FCounterGlayGolemAttack = 0,
4678 }
4679
4680 let golem_melee_range = self.body.map_or(0.0, |b| b.max_radius()) + GOLEM_MELEE_RANGE;
4681 let health_fraction = self.health.map_or(1.0, |h| h.fraction());
4684 let target_speed_cross_sqd = agent
4686 .target
4687 .as_ref()
4688 .map(|t| t.target)
4689 .and_then(|e| read_data.velocities.get(e))
4690 .map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
4691 let line_of_sight_with_target = || {
4692 entities_have_line_of_sight(
4693 self.pos,
4694 self.body,
4695 self.scale,
4696 tgt_data.pos,
4697 tgt_data.body,
4698 tgt_data.scale,
4699 read_data,
4700 )
4701 };
4702
4703 if attack_data.dist_sqrd < golem_melee_range.powi(2) {
4704 if agent.combat_state.counters[ActionStateFCounters::FCounterGlayGolemAttack as usize]
4705 < 7.5
4706 {
4707 controller.push_basic_input(InputKind::Primary);
4709 agent.combat_state.counters
4710 [ActionStateFCounters::FCounterGlayGolemAttack as usize] += read_data.dt.0;
4711 } else {
4712 controller.push_basic_input(InputKind::Ability(1));
4714 if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
4715 {
4716 agent.combat_state.counters
4717 [ActionStateFCounters::FCounterGlayGolemAttack as usize] = 0.0;
4718 }
4719 }
4720 } else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) {
4721 if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
4722 || target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2)
4723 && line_of_sight_with_target()
4724 && attack_data.angle < 45.0
4725 {
4726 controller.push_basic_input(InputKind::Secondary);
4729 } else if health_fraction < 0.7 {
4730 controller.push_basic_input(InputKind::Ability(0));
4733 }
4734 } else if attack_data.dist_sqrd < GOLEM_LONG_RANGE.powi(2) {
4735 if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() {
4736 controller.push_basic_input(InputKind::Ability(1));
4738 } else if health_fraction < 0.7 {
4739 controller.push_basic_input(InputKind::Ability(0));
4742 }
4743 }
4744
4745 self.path_toward_target(
4747 agent,
4748 controller,
4749 tgt_data.pos.0,
4750 read_data,
4751 Path::Separate,
4752 (attack_data.dist_sqrd
4753 < (attack_data.min_attack_dist + GOLEM_MELEE_RANGE / 1.5).powi(2))
4754 .then_some(0.1),
4755 );
4756 }
4757
4758 pub fn handle_tidal_warrior_attack(
4759 &self,
4760 agent: &mut Agent,
4761 controller: &mut Controller,
4762 attack_data: &AttackData,
4763 tgt_data: &TargetData,
4764 read_data: &ReadData,
4765 ) {
4766 const SCUTTLE_RANGE: f32 = 40.0;
4767 const BUBBLE_RANGE: f32 = 20.0;
4768 const MINION_SUMMON_THRESHOLD: f32 = 0.20;
4769
4770 enum ActionStateConditions {
4771 ConditionCounterInitialized = 0,
4772 }
4773
4774 enum ActionStateFCounters {
4775 FCounterMinionSummonThreshold = 0,
4776 }
4777
4778 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4779 let line_of_sight_with_target = || {
4780 entities_have_line_of_sight(
4781 self.pos,
4782 self.body,
4783 self.scale,
4784 tgt_data.pos,
4785 tgt_data.body,
4786 tgt_data.scale,
4787 read_data,
4788 )
4789 };
4790 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
4791 if !agent.combat_state.conditions
4794 [ActionStateConditions::ConditionCounterInitialized as usize]
4795 {
4796 agent.combat_state.counters
4797 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
4798 1.0 - MINION_SUMMON_THRESHOLD;
4799 agent.combat_state.conditions
4800 [ActionStateConditions::ConditionCounterInitialized as usize] = true;
4801 }
4802
4803 if agent.combat_state.counters[ActionStateFCounters::FCounterMinionSummonThreshold as usize]
4804 > health_fraction
4805 {
4806 controller.push_basic_input(InputKind::Ability(1));
4808
4809 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
4810 {
4811 agent.combat_state.counters
4812 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
4813 MINION_SUMMON_THRESHOLD;
4814 }
4815 } else if attack_data.dist_sqrd < SCUTTLE_RANGE.powi(2) {
4816 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4817 {
4818 controller.push_basic_input(InputKind::Secondary);
4820 } else if attack_data.dist_sqrd < BUBBLE_RANGE.powi(2) {
4821 if matches!(self.char_state, CharacterState::BasicBeam(c) if !matches!(c.stage_section, StageSection::Recover) && c.timer < Duration::from_secs(10))
4822 {
4823 controller.push_basic_input(InputKind::Ability(0));
4826 } else if attack_data.in_min_range() && attack_data.angle < 60.0 {
4827 controller.push_basic_input(InputKind::Primary);
4829 } else if attack_data.angle < 30.0 && line_of_sight_with_target() {
4830 controller.push_basic_input(InputKind::Ability(0));
4833 }
4834 } else if attack_data.angle < 90.0 && line_of_sight_with_target() {
4835 controller.push_basic_input(InputKind::Secondary);
4838 }
4839 }
4840 let path = if tgt_data.pos.0.z < self.pos.0.z {
4841 home
4842 } else {
4843 tgt_data.pos.0
4844 };
4845 self.path_toward_target(agent, controller, path, read_data, Path::Partial, None);
4848 }
4849
4850 pub fn handle_yeti_attack(
4851 &self,
4852 agent: &mut Agent,
4853 controller: &mut Controller,
4854 attack_data: &AttackData,
4855 tgt_data: &TargetData,
4856 read_data: &ReadData,
4857 ) {
4858 const ICE_SPIKES_RANGE: f32 = 15.0;
4859 const ICE_BREATH_RANGE: f32 = 10.0;
4860 const ICE_BREATH_TIMER: f32 = 10.0;
4861 const SNOWBALL_MAX_RANGE: f32 = 50.0;
4862
4863 enum ActionStateFCounters {
4864 FCounterYetiAttack = 0,
4865 }
4866
4867 agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize] +=
4868 read_data.dt.0;
4869
4870 if attack_data.dist_sqrd < ICE_BREATH_RANGE.powi(2) {
4871 if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(2))
4872 {
4873 controller.push_basic_input(InputKind::Ability(0));
4875 } else if agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize]
4876 > ICE_BREATH_TIMER
4877 {
4878 controller.push_basic_input(InputKind::Ability(0));
4880
4881 if matches!(self.char_state, CharacterState::BasicBeam(_)) {
4882 agent.combat_state.counters
4884 [ActionStateFCounters::FCounterYetiAttack as usize] = 0.0;
4885 }
4886 } else if attack_data.in_min_range() {
4887 controller.push_basic_input(InputKind::Primary);
4889 } else {
4890 controller.push_basic_input(InputKind::Secondary);
4892 }
4893 } else if attack_data.dist_sqrd < ICE_SPIKES_RANGE.powi(2) && attack_data.angle < 60.0 {
4894 controller.push_basic_input(InputKind::Secondary);
4896 } else if attack_data.dist_sqrd < SNOWBALL_MAX_RANGE.powi(2) && attack_data.angle < 60.0 {
4897 controller.push_basic_input(InputKind::Ability(1));
4899 }
4900
4901 self.path_toward_target(
4903 agent,
4904 controller,
4905 tgt_data.pos.0,
4906 read_data,
4907 Path::Partial,
4908 attack_data.in_min_range().then_some(0.1),
4909 );
4910 }
4911
4912 pub fn handle_rocksnapper_attack(
4913 &self,
4914 agent: &mut Agent,
4915 controller: &mut Controller,
4916 attack_data: &AttackData,
4917 tgt_data: &TargetData,
4918 read_data: &ReadData,
4919 ) {
4920 const LEAP_TIMER: f32 = 3.0;
4921 const DASH_TIMER: f32 = 5.0;
4922 const LEAP_RANGE: f32 = 20.0;
4923 const MELEE_RANGE: f32 = 5.0;
4924
4925 enum ActionStateTimers {
4926 TimerRocksnapperDash = 0,
4927 TimerRocksnapperLeap = 1,
4928 }
4929 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] +=
4930 read_data.dt.0;
4931 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] +=
4932 read_data.dt.0;
4933
4934 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4935 {
4936 controller.push_basic_input(InputKind::Secondary);
4938 } else if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize]
4939 > DASH_TIMER
4940 {
4941 controller.push_basic_input(InputKind::Secondary);
4943
4944 if matches!(self.char_state, CharacterState::DashMelee(_)) {
4945 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] = 0.0;
4947 }
4948 } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) && attack_data.angle < 90.0 {
4949 if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize]
4950 > LEAP_TIMER
4951 {
4952 controller.push_basic_input(InputKind::Ability(0));
4954
4955 if matches!(self.char_state, CharacterState::LeapShockwave(_)) {
4956 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] =
4958 0.0;
4959 }
4960 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4961 controller.push_basic_input(InputKind::Primary);
4963 }
4964 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
4965 controller.push_basic_input(InputKind::Primary);
4967 }
4968
4969 self.path_toward_target(
4971 agent,
4972 controller,
4973 tgt_data.pos.0,
4974 read_data,
4975 Path::Partial,
4976 None,
4977 );
4978 }
4979
4980 pub fn handle_roshwalr_attack(
4981 &self,
4982 agent: &mut Agent,
4983 controller: &mut Controller,
4984 attack_data: &AttackData,
4985 tgt_data: &TargetData,
4986 read_data: &ReadData,
4987 ) {
4988 const SLOW_CHARGE_RANGE: f32 = 12.5;
4989 const SHOCKWAVE_RANGE: f32 = 12.5;
4990 const SHOCKWAVE_TIMER: f32 = 15.0;
4991 const MELEE_RANGE: f32 = 4.0;
4992
4993 enum ActionStateFCounters {
4994 FCounterRoshwalrAttack = 0,
4995 }
4996
4997 agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] +=
4998 read_data.dt.0;
4999 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5000 {
5001 controller.push_basic_input(InputKind::Ability(0));
5003 } else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 270.0 {
5004 if agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize]
5005 > SHOCKWAVE_TIMER
5006 {
5007 controller.push_basic_input(InputKind::Ability(0));
5009
5010 if matches!(self.char_state, CharacterState::Shockwave(_)) {
5011 agent.combat_state.counters
5013 [ActionStateFCounters::FCounterRoshwalrAttack as usize] = 0.0;
5014 }
5015 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5016 controller.push_basic_input(InputKind::Primary);
5018 }
5019 } else if attack_data.dist_sqrd > SLOW_CHARGE_RANGE.powi(2) {
5020 controller.push_basic_input(InputKind::Secondary);
5022 }
5023
5024 self.path_toward_target(
5026 agent,
5027 controller,
5028 tgt_data.pos.0,
5029 read_data,
5030 Path::Partial,
5031 None,
5032 );
5033 }
5034
5035 pub fn handle_harvester_attack(
5036 &self,
5037 agent: &mut Agent,
5038 controller: &mut Controller,
5039 attack_data: &AttackData,
5040 tgt_data: &TargetData,
5041 read_data: &ReadData,
5042 rng: &mut impl Rng,
5043 ) {
5044 const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
5058 const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
5059 const PATH_RANGE_FACTOR: f32 = 0.4; const SCYTHE_RANGE_FACTOR: f32 = 0.75; const SCYTHE_AIM_FACTOR: f32 = 0.7;
5062 const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
5063 const FIREBREATH_AIM_FACTOR: f32 = 0.8;
5064 const FIREBREATH_TIME_LIMIT: f32 = 4.0;
5065 const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; const FIREBREATH_COOLDOWN: f32 = 3.5;
5067 const PUMPKIN_RANGE_FACTOR: f32 = 0.75;
5068 const CLOSE_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 7.0]; const MID_MIXUP_COOLDOWN_SPAN: [f32; 2] = [1.5, 4.5]; const FAR_PUMPKIN_COOLDOWN_SPAN: [f32; 2] = [3.0, 5.0]; const HAS_SUMMONED_FIRST_VINES: usize = 0;
5074 const HAS_SUMMONED_SECOND_VINES: usize = 1;
5075 const FIREBREATH: usize = 0;
5077 const MIXUP: usize = 1;
5078 const FAR_PUMPKIN: usize = 2;
5079 const CLOSE_MIXUP_COOLDOWN: usize = 0;
5081 const MID_MIXUP_COOLDOWN: usize = 1;
5082 const FAR_PUMPKIN_COOLDOWN: usize = 2;
5083
5084 let line_of_sight_with_target = || {
5086 entities_have_line_of_sight(
5087 self.pos,
5088 self.body,
5089 self.scale,
5090 tgt_data.pos,
5091 tgt_data.body,
5092 tgt_data.scale,
5093 read_data,
5094 )
5095 };
5096
5097 let (scythe_range, scythe_angle) = {
5100 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
5101 self.extract_ability(AbilityInput::Primary)
5102 {
5103 (range, angle)
5104 } else {
5105 (0.0, 0.0)
5106 }
5107 };
5108 let (firebreath_range, firebreath_angle) = {
5109 if let Some(AbilityData::BasicBeam { range, angle, .. }) =
5110 self.extract_ability(AbilityInput::Secondary)
5111 {
5112 (range, angle)
5113 } else {
5114 (0.0, 0.0)
5115 }
5116 };
5117 let pumpkin_speed = {
5118 if let Some(AbilityData::BasicRanged {
5119 projectile_speed, ..
5120 }) = self.extract_ability(AbilityInput::Auxiliary(0))
5121 {
5122 projectile_speed
5123 } else {
5124 0.0
5125 }
5126 };
5127 let pumpkin_max_range =
5129 projectile_flat_range(pumpkin_speed, self.body.map_or(0.0, |b| b.height()));
5130
5131 let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
5133 let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
5134 let is_in_summon_recovery = matches!(self.char_state, CharacterState::SpriteSummon(data) if matches!(data.stage_section, StageSection::Recover));
5135 let firebreath_timer = if let CharacterState::BasicBeam(data) = self.char_state {
5136 data.timer
5137 } else {
5138 Default::default()
5139 };
5140 let is_using_mixup = is_using_firebreath || is_using_pumpkin;
5141
5142 if !agent.combat_state.initialized {
5144 agent.combat_state.initialized = true;
5145 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5146 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5147 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5148 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5149 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5150 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5151 }
5152
5153 if is_in_summon_recovery {
5157 agent.combat_state.timers[FIREBREATH] = 0.0;
5159 agent.combat_state.timers[MIXUP] = 0.0;
5160 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5161 } else {
5162 if is_using_firebreath {
5164 agent.combat_state.timers[FIREBREATH] = 0.0;
5165 } else {
5166 agent.combat_state.timers[FIREBREATH] += read_data.dt.0;
5167 }
5168 if is_using_mixup {
5169 agent.combat_state.timers[MIXUP] = 0.0;
5170 } else {
5171 agent.combat_state.timers[MIXUP] += read_data.dt.0;
5172 }
5173 if is_using_pumpkin {
5174 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5175 } else {
5176 agent.combat_state.timers[FAR_PUMPKIN] += read_data.dt.0;
5177 }
5178 }
5179
5180 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5182 if health_fraction < SECOND_VINE_CREATION_THRESHOLD
5184 && !agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES]
5185 {
5186 controller.push_basic_input(InputKind::Ability(2));
5188 if is_in_summon_recovery {
5190 agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES] = true;
5191 }
5192 }
5193 else if health_fraction < FIRST_VINE_CREATION_THRESHOLD
5195 && !agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES]
5196 {
5197 controller.push_basic_input(InputKind::Ability(1));
5199 if is_in_summon_recovery {
5201 agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES] = true;
5202 }
5203 }
5204 else if attack_data.dist_sqrd
5206 < (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
5207 {
5208 if is_using_firebreath
5210 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
5211 {
5212 controller.push_basic_input(InputKind::Secondary);
5213 }
5214 if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
5216 if agent.combat_state.timers[MIXUP]
5218 > agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN]
5219 {
5221 if agent.combat_state.timers[FIREBREATH] < FIREBREATH_COOLDOWN {
5223 controller.push_basic_input(InputKind::Ability(0));
5224 }
5225 else if rng.gen_bool(0.5) {
5227 controller.push_basic_input(InputKind::Secondary);
5228 } else {
5229 controller.push_basic_input(InputKind::Ability(0));
5230 }
5231 if is_using_mixup {
5233 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5234 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5235 }
5236 }
5237 else {
5239 controller.push_basic_input(InputKind::Primary);
5240 }
5241 }
5242 } else if attack_data.dist_sqrd < firebreath_range.powi(2) {
5244 #[expect(clippy::if_same_then_else)]
5246 if is_using_firebreath
5247 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
5248 {
5249 controller.push_basic_input(InputKind::Secondary);
5250 }
5251 else if attack_data.dist_sqrd < (firebreath_range * FIREBREATH_RANGE_FACTOR).powi(2)
5253 && attack_data.angle < firebreath_angle * FIREBREATH_AIM_FACTOR
5254 && agent.combat_state.timers[FIREBREATH] > FIREBREATH_COOLDOWN
5255 {
5256 controller.push_basic_input(InputKind::Secondary);
5257 }
5258 else if agent.combat_state.timers[MIXUP]
5260 > agent.combat_state.counters[MID_MIXUP_COOLDOWN]
5261 {
5262 controller.push_basic_input(InputKind::Ability(0));
5263 if is_using_pumpkin {
5265 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5266 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5267 }
5268 }
5269 }
5270 else if attack_data.dist_sqrd < (pumpkin_max_range * PUMPKIN_RANGE_FACTOR).powi(2)
5272 && agent.combat_state.timers[FAR_PUMPKIN]
5273 > agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN]
5274 && line_of_sight_with_target()
5275 {
5276 controller.push_basic_input(InputKind::Ability(0));
5278 if is_using_pumpkin {
5280 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5281 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5282 }
5283 }
5284
5285 if attack_data.dist_sqrd
5288 > (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
5289 {
5290 self.path_toward_target(
5291 agent,
5292 controller,
5293 tgt_data.pos.0,
5294 read_data,
5295 Path::Partial,
5296 None,
5297 );
5298 }
5299 else if attack_data.angle > 0.0 {
5301 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
5303 .xy()
5304 .try_normalized()
5305 .unwrap_or_else(Vec2::zero)
5306 * 0.001; }
5308 }
5309
5310 pub fn handle_frostgigas_attack(
5311 &self,
5312 agent: &mut Agent,
5313 controller: &mut Controller,
5314 attack_data: &AttackData,
5315 tgt_data: &TargetData,
5316 read_data: &ReadData,
5317 rng: &mut impl Rng,
5318 ) {
5319 const GIGAS_MELEE_RANGE: f32 = 12.0;
5320 const GIGAS_SPIKE_RANGE: f32 = 16.0;
5321 const ICEBOMB_RANGE: f32 = 70.0;
5322 const GIGAS_LEAP_RANGE: f32 = 50.0;
5323 const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
5324 const FLASHFREEZE_RANGE: f32 = 30.;
5325
5326 enum ActionStateTimers {
5327 AttackChange,
5328 Bonk,
5329 }
5330
5331 enum ActionStateFCounters {
5332 FCounterMinionSummonThreshold = 0,
5333 }
5334
5335 enum ActionStateICounters {
5336 CurrentAbility = 0,
5341 }
5342
5343 let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0);
5344 let remote_spikes_action = || ControlAction::StartInput {
5345 input: InputKind::Ability(5),
5346 target_entity: None,
5347 select_pos: Some(tgt_data.pos.0),
5348 };
5349
5350 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5351 if !agent.combat_state.initialized {
5354 agent.combat_state.counters
5355 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5356 1.0 - MINION_SUMMON_THRESHOLD;
5357 agent.combat_state.initialized = true;
5358 }
5359
5360 if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 {
5362 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
5363 } else {
5364 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
5365 }
5366 agent.combat_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0;
5367
5368 if health_fraction
5369 < agent.combat_state.counters
5370 [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5371 {
5372 controller.push_basic_input(InputKind::Ability(3));
5374
5375 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5376 {
5377 agent.combat_state.counters
5378 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5379 MINION_SUMMON_THRESHOLD;
5380 }
5381 } else if let Some(ability) = Some(
5383 &mut agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize],
5384 )
5385 .filter(|i| **i != 0)
5386 {
5387 if *ability == 3 && should_use_targeted_spikes() {
5388 *ability = 5
5389 };
5390
5391 let reset = match ability {
5392 1 => {
5394 controller.push_basic_input(InputKind::Ability(1));
5395 matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
5396 },
5397 2 => {
5399 controller.push_basic_input(InputKind::Ability(4));
5400 matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover))
5401 },
5402 3 => {
5404 controller.push_basic_input(InputKind::Ability(0));
5405 matches!(self.char_state, CharacterState::SpriteSummon(c)
5406 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner)))
5407 },
5408 4 => {
5410 controller.push_basic_input(InputKind::Ability(7));
5411 matches!(self.char_state, CharacterState::RapidMelee(c) if matches!(c.stage_section, StageSection::Recover))
5412 },
5413 5 => {
5415 controller.push_action(remote_spikes_action());
5416 matches!(self.char_state, CharacterState::SpriteSummon(c)
5417 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target)))
5418 },
5419 6 => {
5421 controller.push_basic_input(InputKind::Ability(2));
5422 matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5423 },
5424 _ => true,
5426 };
5427
5428 if reset {
5429 *ability = 0;
5430 }
5431 } else if attack_data.dist_sqrd > 5f32.powi(2)
5435 && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6
5437 && rng.gen_bool((0.2 * read_data.dt.0).min(1.0) as f64)
5439 {
5440 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5441 rng.gen_range(5..=6);
5442 } else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
5443 if agent.combat_state.timers[ActionStateTimers::Bonk as usize] > 10. {
5445 controller.push_basic_input(InputKind::Ability(6));
5446
5447 if matches!(self.char_state, CharacterState::BasicMelee(c)
5448 if matches!(c.stage_section, StageSection::Recover) &&
5449 c.static_data.ability_info.ability.is_some_and(|meta| matches!(meta.ability, Ability::MainWeaponAux(6)))
5450 ) {
5451 agent.combat_state.timers[ActionStateTimers::Bonk as usize] =
5452 rng.gen_range(0.0..3.0);
5453 }
5454 } else if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 4.0
5456 && rng.gen_bool(0.1 * read_data.dt.0.min(1.0) as f64)
5457 {
5458 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5459 rng.gen_range(1..=4);
5460 } else if attack_data.angle > 90.0
5463 || agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 5.0
5464 {
5465 if attack_data.angle > 120.0 {
5467 agent.combat_state.int_counters
5468 [ActionStateICounters::CurrentAbility as usize] = 4;
5469 } else {
5470 controller.push_basic_input(InputKind::Secondary);
5471 }
5472 } else {
5473 controller.push_basic_input(InputKind::Primary);
5474 }
5475 } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
5476 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
5477 {
5478 if should_use_targeted_spikes() {
5479 controller.push_action(remote_spikes_action());
5480 } else {
5481 controller.push_basic_input(InputKind::Ability(0));
5482 }
5483 } else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
5484 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
5485 {
5486 controller.push_basic_input(InputKind::Ability(4));
5487 } else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2)
5489 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 3.0
5490 {
5491 controller.push_basic_input(InputKind::Ability(1));
5492 } else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2)
5493 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 3.0
5494 {
5495 controller.push_basic_input(InputKind::Ability(2));
5496 } else {
5498 controller.push_action(remote_spikes_action());
5499 }
5500
5501 self.path_toward_target(
5503 agent,
5504 controller,
5505 tgt_data.pos.0,
5506 read_data,
5507 Path::Partial,
5508 attack_data.in_min_range().then_some(0.1),
5509 );
5510 }
5511
5512 pub fn handle_boreal_hammer_attack(
5513 &self,
5514 agent: &mut Agent,
5515 controller: &mut Controller,
5516 attack_data: &AttackData,
5517 tgt_data: &TargetData,
5518 read_data: &ReadData,
5519 rng: &mut impl Rng,
5520 ) {
5521 enum ActionStateTimers {
5522 TimerHandleHammerAttack = 0,
5523 }
5524
5525 let has_energy = |need| self.energy.current() > need;
5526
5527 let use_leap = |controller: &mut Controller| {
5528 controller.push_basic_input(InputKind::Ability(0));
5529 };
5530
5531 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] +=
5532 read_data.dt.0;
5533
5534 if attack_data.in_min_range() && attack_data.angle < 45.0 {
5535 controller.inputs.move_dir = Vec2::zero();
5536 if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0
5537 {
5538 controller.push_cancel_input(InputKind::Secondary);
5539 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] =
5540 0.0;
5541 } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize]
5542 > 3.0
5543 {
5544 controller.push_basic_input(InputKind::Secondary);
5545 } else if has_energy(50.0) && rng.gen_bool(0.9) {
5546 use_leap(controller);
5547 } else {
5548 controller.push_basic_input(InputKind::Primary);
5549 }
5550 } else {
5551 self.path_toward_target(
5552 agent,
5553 controller,
5554 tgt_data.pos.0,
5555 read_data,
5556 Path::Separate,
5557 None,
5558 );
5559
5560 if attack_data.dist_sqrd < 32.0f32.powi(2)
5561 && entities_have_line_of_sight(
5562 self.pos,
5563 self.body,
5564 self.scale,
5565 tgt_data.pos,
5566 tgt_data.body,
5567 tgt_data.scale,
5568 read_data,
5569 )
5570 {
5571 if rng.gen_bool(0.5) && has_energy(50.0) {
5572 use_leap(controller);
5573 } else if agent.combat_state.timers
5574 [ActionStateTimers::TimerHandleHammerAttack as usize]
5575 > 2.0
5576 {
5577 controller.push_basic_input(InputKind::Secondary);
5578 } else if agent.combat_state.timers
5579 [ActionStateTimers::TimerHandleHammerAttack as usize]
5580 > 4.0
5581 {
5582 controller.push_cancel_input(InputKind::Secondary);
5583 agent.combat_state.timers
5584 [ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0;
5585 }
5586 }
5587 }
5588 }
5589
5590 pub fn handle_boreal_bow_attack(
5591 &self,
5592 agent: &mut Agent,
5593 controller: &mut Controller,
5594 attack_data: &AttackData,
5595 tgt_data: &TargetData,
5596 read_data: &ReadData,
5597 rng: &mut impl Rng,
5598 ) {
5599 let line_of_sight_with_target = || {
5600 entities_have_line_of_sight(
5601 self.pos,
5602 self.body,
5603 self.scale,
5604 tgt_data.pos,
5605 tgt_data.body,
5606 tgt_data.scale,
5607 read_data,
5608 )
5609 };
5610
5611 let has_energy = |need| self.energy.current() > need;
5612
5613 let use_trap = |controller: &mut Controller| {
5614 controller.push_basic_input(InputKind::Ability(0));
5615 };
5616
5617 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5618 if rng.gen_bool(0.5) && has_energy(15.0) {
5619 controller.push_basic_input(InputKind::Secondary);
5620 } else if attack_data.angle < 15.0 {
5621 controller.push_basic_input(InputKind::Primary);
5622 }
5623 } else if attack_data.dist_sqrd < (4.0 * attack_data.min_attack_dist).powi(2)
5624 && line_of_sight_with_target()
5625 {
5626 if rng.gen_bool(0.5) && has_energy(15.0) {
5627 controller.push_basic_input(InputKind::Secondary);
5628 } else if has_energy(20.0) {
5629 use_trap(controller);
5630 }
5631 }
5632
5633 if has_energy(50.0) {
5634 if attack_data.dist_sqrd < (10.0 * attack_data.min_attack_dist).powi(2) {
5635 if let Some((bearing, speed)) = agent.chaser.chase(
5637 &*read_data.terrain,
5638 self.pos.0,
5639 self.vel.0,
5640 tgt_data.pos.0,
5641 TraversalConfig {
5642 min_tgt_dist: 1.25,
5643 ..self.traversal_config
5644 },
5645 ) {
5646 if line_of_sight_with_target() && attack_data.angle < 45.0 {
5647 controller.inputs.move_dir = bearing
5648 .xy()
5649 .rotated_z(rng.gen_range(0.5..1.57))
5650 .try_normalized()
5651 .unwrap_or_else(Vec2::zero)
5652 * 2.0
5653 * speed;
5654 } else {
5655 controller.inputs.move_dir =
5657 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5658 self.jump_if(bearing.z > 1.5, controller);
5659 controller.inputs.move_z = bearing.z;
5660 }
5661 }
5662 } else {
5663 self.path_toward_target(
5665 agent,
5666 controller,
5667 tgt_data.pos.0,
5668 read_data,
5669 Path::Partial,
5670 None,
5671 );
5672 }
5673 } else {
5674 self.path_toward_target(
5676 agent,
5677 controller,
5678 tgt_data.pos.0,
5679 read_data,
5680 Path::Partial,
5681 None,
5682 );
5683 }
5684 }
5685
5686 pub fn handle_cardinal_attack(
5687 &self,
5688 agent: &mut Agent,
5689 controller: &mut Controller,
5690 attack_data: &AttackData,
5691 tgt_data: &TargetData,
5692 read_data: &ReadData,
5693 rng: &mut impl Rng,
5694 ) {
5695 const DESIRED_ENERGY_LEVEL: f32 = 50.0;
5696 const DESIRED_COMBO_LEVEL: u32 = 8;
5697 const MINION_SUMMON_THRESHOLD: f32 = 0.10;
5698
5699 enum ActionStateConditions {
5700 ConditionCounterInitialized = 0,
5701 }
5702
5703 enum ActionStateFCounters {
5704 FCounterHealthThreshold = 0,
5705 }
5706
5707 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5708 if !agent.combat_state.conditions
5711 [ActionStateConditions::ConditionCounterInitialized as usize]
5712 {
5713 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
5714 1.0 - MINION_SUMMON_THRESHOLD;
5715 agent.combat_state.conditions
5716 [ActionStateConditions::ConditionCounterInitialized as usize] = true;
5717 }
5718
5719 if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
5720 > health_fraction
5721 {
5722 controller.push_basic_input(InputKind::Ability(1));
5724
5725 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5726 {
5727 agent.combat_state.counters
5728 [ActionStateFCounters::FCounterHealthThreshold as usize] -=
5729 MINION_SUMMON_THRESHOLD;
5730 }
5731 }
5732 else if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
5734 && entities_have_line_of_sight(
5735 self.pos,
5736 self.body,
5737 self.scale,
5738 tgt_data.pos,
5739 tgt_data.body,
5740 tgt_data.scale,
5741 read_data,
5742 )
5743 {
5744 if self.energy.current() > DESIRED_ENERGY_LEVEL
5747 && read_data
5748 .combos
5749 .get(*self.entity)
5750 .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
5751 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
5752 buff.iter_kind(BuffKind::Regeneration)
5753 .peekable()
5754 .peek()
5755 .is_some()
5756 })
5757 {
5758 controller.push_basic_input(InputKind::Secondary);
5760 } else if self
5761 .skill_set
5762 .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
5763 && self.energy.current() > DESIRED_ENERGY_LEVEL
5764 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
5765 buff.iter_kind(BuffKind::ProtectingWard)
5766 .peekable()
5767 .peek()
5768 .is_some()
5769 })
5770 {
5771 controller.push_basic_input(InputKind::Ability(0));
5774 } else {
5775 controller.push_basic_input(InputKind::Primary);
5778 }
5779 } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5780 if self.body.is_some_and(|b| b.is_humanoid())
5781 && self.energy.current()
5782 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
5783 && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
5784 {
5785 controller.push_basic_input(InputKind::Ability(0));
5787 } else if attack_data.angle < 15.0 {
5788 controller.push_basic_input(InputKind::Primary);
5789 }
5790 }
5791 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5794 if let Some((bearing, speed)) = agent.chaser.chase(
5796 &*read_data.terrain,
5797 self.pos.0,
5798 self.vel.0,
5799 tgt_data.pos.0,
5800 TraversalConfig {
5801 min_tgt_dist: 1.25,
5802 ..self.traversal_config
5803 },
5804 ) {
5805 controller.inputs.move_dir =
5806 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5807 }
5808 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
5809 if let Some((bearing, speed)) = agent.chaser.chase(
5811 &*read_data.terrain,
5812 self.pos.0,
5813 self.vel.0,
5814 tgt_data.pos.0,
5815 TraversalConfig {
5816 min_tgt_dist: 1.25,
5817 ..self.traversal_config
5818 },
5819 ) {
5820 if entities_have_line_of_sight(
5821 self.pos,
5822 self.body,
5823 self.scale,
5824 tgt_data.pos,
5825 tgt_data.body,
5826 tgt_data.scale,
5827 read_data,
5828 ) && attack_data.angle < 45.0
5829 {
5830 controller.inputs.move_dir = bearing
5831 .xy()
5832 .rotated_z(rng.gen_range(0.5..1.57))
5833 .try_normalized()
5834 .unwrap_or_else(Vec2::zero)
5835 * speed;
5836 } else {
5837 controller.inputs.move_dir =
5839 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5840 self.jump_if(bearing.z > 1.5, controller);
5841 controller.inputs.move_z = bearing.z;
5842 }
5843 }
5844 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
5846 && !matches!(self.char_state, CharacterState::BasicAura(_))
5847 && attack_data.dist_sqrd < 16.0f32.powi(2)
5848 && rng.gen::<f32>() < 0.01
5849 {
5850 controller.push_basic_input(InputKind::Roll);
5851 }
5852 } else {
5853 self.path_toward_target(
5855 agent,
5856 controller,
5857 tgt_data.pos.0,
5858 read_data,
5859 Path::Partial,
5860 None,
5861 );
5862 }
5863 }
5864
5865 pub fn handle_sea_bishop_attack(
5866 &self,
5867 agent: &mut Agent,
5868 controller: &mut Controller,
5869 attack_data: &AttackData,
5870 tgt_data: &TargetData,
5871 read_data: &ReadData,
5872 rng: &mut impl Rng,
5873 ) {
5874 let line_of_sight_with_target = || {
5875 entities_have_line_of_sight(
5876 self.pos,
5877 self.body,
5878 self.scale,
5879 tgt_data.pos,
5880 tgt_data.body,
5881 tgt_data.scale,
5882 read_data,
5883 )
5884 };
5885
5886 enum ActionStateTimers {
5887 TimerBeam = 0,
5888 }
5889 if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 6.0 {
5890 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
5891 } else {
5892 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
5893 }
5894
5895 if line_of_sight_with_target()
5897 && agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 3.0
5898 {
5899 controller.push_basic_input(InputKind::Primary);
5900 }
5901 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5904 if let Some((bearing, speed)) = agent.chaser.chase(
5906 &*read_data.terrain,
5907 self.pos.0,
5908 self.vel.0,
5909 tgt_data.pos.0,
5910 TraversalConfig {
5911 min_tgt_dist: 1.25,
5912 ..self.traversal_config
5913 },
5914 ) {
5915 controller.inputs.move_dir =
5916 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5917 }
5918 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
5919 if let Some((bearing, speed)) = agent.chaser.chase(
5921 &*read_data.terrain,
5922 self.pos.0,
5923 self.vel.0,
5924 tgt_data.pos.0,
5925 TraversalConfig {
5926 min_tgt_dist: 1.25,
5927 ..self.traversal_config
5928 },
5929 ) {
5930 if line_of_sight_with_target() && attack_data.angle < 45.0 {
5931 controller.inputs.move_dir = bearing
5932 .xy()
5933 .rotated_z(rng.gen_range(0.5..1.57))
5934 .try_normalized()
5935 .unwrap_or_else(Vec2::zero)
5936 * speed;
5937 } else {
5938 controller.inputs.move_dir =
5940 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5941 self.jump_if(bearing.z > 1.5, controller);
5942 controller.inputs.move_z = bearing.z;
5943 }
5944 }
5945 } else {
5946 self.path_toward_target(
5948 agent,
5949 controller,
5950 tgt_data.pos.0,
5951 read_data,
5952 Path::Partial,
5953 None,
5954 );
5955 }
5956 }
5957
5958 pub fn handle_cursekeeper_attack(
5959 &self,
5960 agent: &mut Agent,
5961 controller: &mut Controller,
5962 attack_data: &AttackData,
5963 tgt_data: &TargetData,
5964 read_data: &ReadData,
5965 rng: &mut impl Rng,
5966 ) {
5967 enum ActionStateTimers {
5968 TimerBeam,
5969 TimerSummon,
5970 SelectSummon,
5971 }
5972 if tgt_data.pos.0.z - self.pos.0.z > 3.5 {
5973 controller.push_action(ControlAction::StartInput {
5974 input: InputKind::Ability(4),
5975 target_entity: agent
5976 .target
5977 .as_ref()
5978 .and_then(|t| read_data.uids.get(t.target))
5979 .copied(),
5980 select_pos: None,
5981 });
5982 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 12.0 {
5983 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
5984 } else {
5985 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
5986 }
5987
5988 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
5989 {
5990 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] = 0.0;
5991 agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] =
5992 rng.gen_range(0..=3) as f32;
5993 } else {
5994 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] += read_data.dt.0;
5995 }
5996
5997 if agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] > 32.0 {
5998 match agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] as i32 {
5999 0 => controller.push_basic_input(InputKind::Ability(0)),
6000 1 => controller.push_basic_input(InputKind::Ability(1)),
6001 2 => controller.push_basic_input(InputKind::Ability(2)),
6002 _ => controller.push_basic_input(InputKind::Ability(3)),
6003 }
6004 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 6.0 {
6005 controller.push_basic_input(InputKind::Ability(5));
6006 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 9.0 {
6007 controller.push_basic_input(InputKind::Primary);
6008 } else {
6009 controller.push_basic_input(InputKind::Secondary);
6010 }
6011
6012 if attack_data.dist_sqrd > 10_f32.powi(2)
6013 || agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 4.0
6014 {
6015 self.path_toward_target(
6016 agent,
6017 controller,
6018 tgt_data.pos.0,
6019 read_data,
6020 Path::Full,
6021 None,
6022 );
6023 }
6024 }
6025
6026 pub fn handle_shamanic_spirit_attack(
6027 &self,
6028 agent: &mut Agent,
6029 controller: &mut Controller,
6030 attack_data: &AttackData,
6031 tgt_data: &TargetData,
6032 read_data: &ReadData,
6033 ) {
6034 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
6035 controller.push_action(ControlAction::StartInput {
6036 input: InputKind::Secondary,
6037 target_entity: agent
6038 .target
6039 .as_ref()
6040 .and_then(|t| read_data.uids.get(t.target))
6041 .copied(),
6042 select_pos: None,
6043 });
6044 } else if attack_data.in_min_range() && attack_data.angle < 30.0 {
6045 controller.push_basic_input(InputKind::Primary);
6046 controller.inputs.move_dir = Vec2::zero();
6047 } else {
6048 self.path_toward_target(
6049 agent,
6050 controller,
6051 tgt_data.pos.0,
6052 read_data,
6053 Path::Full,
6054 None,
6055 );
6056 }
6057 }
6058
6059 pub fn handle_cursekeeper_fake_attack(
6060 &self,
6061 controller: &mut Controller,
6062 attack_data: &AttackData,
6063 ) {
6064 if attack_data.dist_sqrd < 25_f32.powi(2) {
6065 controller.push_basic_input(InputKind::Primary);
6066 }
6067 }
6068
6069 pub fn handle_karkatha_attack(
6070 &self,
6071 agent: &mut Agent,
6072 controller: &mut Controller,
6073 attack_data: &AttackData,
6074 tgt_data: &TargetData,
6075 read_data: &ReadData,
6076 _rng: &mut impl Rng,
6077 ) {
6078 enum ActionStateTimers {
6079 RiposteTimer,
6080 SummonTimer,
6081 }
6082
6083 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] += read_data.dt.0;
6084 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] += read_data.dt.0;
6085 if matches!(self.char_state, CharacterState::RiposteMelee(c) if !matches!(c.stage_section, StageSection::Recover))
6086 {
6087 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] = 0.0;
6089 }
6090 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
6091 {
6092 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] = 0.0;
6094 }
6095 let home = agent.patrol_origin.unwrap_or(self.pos.0);
6097 let dest = if tgt_data.pos.0.z < self.pos.0.z {
6098 home
6099 } else {
6100 tgt_data.pos.0
6101 };
6102 if attack_data.in_min_range() {
6103 if agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] > 3.0 {
6104 controller.push_basic_input(InputKind::Ability(2));
6105 } else {
6106 controller.push_basic_input(InputKind::Primary);
6107 };
6108 } else if attack_data.dist_sqrd < 20.0_f32.powi(2) {
6109 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] > 20.0 {
6110 controller.push_basic_input(InputKind::Ability(1));
6111 } else {
6112 controller.push_basic_input(InputKind::Secondary);
6113 }
6114 } else if attack_data.dist_sqrd < 30.0_f32.powi(2) {
6115 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] < 10.0 {
6116 self.path_toward_target(
6117 agent,
6118 controller,
6119 tgt_data.pos.0,
6120 read_data,
6121 Path::Full,
6122 None,
6123 );
6124 } else {
6125 controller.push_basic_input(InputKind::Ability(0));
6126 }
6127 } else {
6128 self.path_toward_target(agent, controller, dest, read_data, Path::Full, None);
6129 }
6130 }
6131
6132 pub fn handle_dagon_attack(
6133 &self,
6134 agent: &mut Agent,
6135 controller: &mut Controller,
6136 attack_data: &AttackData,
6137 tgt_data: &TargetData,
6138 read_data: &ReadData,
6139 ) {
6140 enum ActionStateTimers {
6141 TimerDagon = 0,
6142 }
6143 let line_of_sight_with_target = || {
6144 entities_have_line_of_sight(
6145 self.pos,
6146 self.body,
6147 self.scale,
6148 tgt_data.pos,
6149 tgt_data.body,
6150 tgt_data.scale,
6151 read_data,
6152 )
6153 };
6154 let home = agent.patrol_origin.unwrap_or(self.pos.0);
6156 let exit = Vec3::new(home.x - 6.0, home.y - 6.0, home.z);
6157 let (station_0, station_1) = (exit + 12.0, exit - 12.0);
6158 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.5 {
6159 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] = 0.0;
6160 }
6161 if !line_of_sight_with_target()
6162 && (tgt_data.pos.0 - exit).xy().magnitude_squared() < (10.0_f32).powi(2)
6163 {
6164 let station = if (tgt_data.pos.0 - station_0).xy().magnitude_squared()
6165 < (tgt_data.pos.0 - station_1).xy().magnitude_squared()
6166 {
6167 station_0
6168 } else {
6169 station_1
6170 };
6171 self.path_toward_target(agent, controller, station, read_data, Path::Full, None);
6172 }
6173 else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6175 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6176 controller.push_basic_input(InputKind::Primary);
6177 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6178 } else {
6179 controller.push_basic_input(InputKind::Secondary);
6180 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6181 }
6182 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6184 controller.inputs.move_dir = Vec2::zero();
6185 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6186 controller.push_basic_input(InputKind::Primary);
6187 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6188 } else {
6189 controller.push_basic_input(InputKind::Ability(1));
6190 }
6191 } else if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) {
6192 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6194 controller.push_basic_input(InputKind::Primary);
6195 } else {
6196 controller.push_basic_input(InputKind::Ability(2));
6197 }
6198 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6199 } else if line_of_sight_with_target() {
6200 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6202 controller.push_basic_input(InputKind::Primary);
6203 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6204 } else {
6205 controller.push_basic_input(InputKind::Ability(0));
6206 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6207 }
6208 }
6209 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6211 Path::Separate
6212 } else {
6213 Path::Partial
6214 };
6215 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
6216 }
6217
6218 pub fn handle_snaretongue_attack(
6219 &self,
6220 agent: &mut Agent,
6221 controller: &mut Controller,
6222 attack_data: &AttackData,
6223 read_data: &ReadData,
6224 ) {
6225 enum Timers {
6226 TimerAttack = 0,
6227 }
6228 let attack_timer = &mut agent.combat_state.timers[Timers::TimerAttack as usize];
6229 if *attack_timer > 2.5 {
6230 *attack_timer = 0.0;
6231 }
6232 if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
6234 if *attack_timer > 0.5 {
6235 controller.push_basic_input(InputKind::Primary);
6236 *attack_timer += read_data.dt.0;
6237 } else {
6238 controller.push_basic_input(InputKind::Secondary);
6239 *attack_timer += read_data.dt.0;
6240 }
6241 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6243 controller.inputs.move_dir = Vec2::zero();
6244 if *attack_timer > 2.0 {
6245 controller.push_basic_input(InputKind::Ability(0));
6246 *attack_timer += read_data.dt.0;
6247 } else {
6248 controller.push_basic_input(InputKind::Ability(1));
6249 }
6250 } else {
6251 if *attack_timer > 1.0 {
6253 controller.push_basic_input(InputKind::Ability(0));
6254 *attack_timer += read_data.dt.0;
6255 } else {
6256 controller.push_basic_input(InputKind::Ability(2));
6257 *attack_timer += read_data.dt.0;
6258 }
6259 }
6260 }
6261
6262 pub fn handle_deadwood(
6263 &self,
6264 agent: &mut Agent,
6265 controller: &mut Controller,
6266 attack_data: &AttackData,
6267 tgt_data: &TargetData,
6268 read_data: &ReadData,
6269 ) {
6270 const BEAM_RANGE: f32 = 20.0;
6271 const BEAM_TIME: Duration = Duration::from_secs(3);
6272 if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
6274 {
6275 controller.push_basic_input(InputKind::Secondary);
6277 controller.inputs.move_dir = self.ori.look_vec().xy();
6278 } else if attack_data.in_min_range() && attack_data.angle_xy < 10.0 {
6279 controller.push_basic_input(InputKind::Secondary);
6281 } else if matches!(self.char_state, CharacterState::BasicBeam(s) if s.stage_section != StageSection::Recover && s.timer < BEAM_TIME)
6282 {
6283 controller.push_basic_input(InputKind::Primary);
6285 } else if attack_data.dist_sqrd < BEAM_RANGE.powi(2) {
6286 if attack_data.angle_xy < 5.0 {
6288 controller.push_basic_input(InputKind::Primary);
6289 } else {
6290 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6292 .xy()
6293 .try_normalized()
6294 .unwrap_or_else(Vec2::zero)
6295 * 0.01;
6296 }
6297 } else {
6298 self.path_toward_target(
6300 agent,
6301 controller,
6302 tgt_data.pos.0,
6303 read_data,
6304 Path::Partial,
6305 None,
6306 );
6307 }
6308 }
6309
6310 pub fn handle_mandragora(
6311 &self,
6312 agent: &mut Agent,
6313 controller: &mut Controller,
6314 attack_data: &AttackData,
6315 tgt_data: &TargetData,
6316 read_data: &ReadData,
6317 ) {
6318 const SCREAM_RANGE: f32 = 10.0; enum ActionStateFCounters {
6321 FCounterHealthThreshold = 0,
6322 }
6323
6324 enum ActionStateConditions {
6325 ConditionHasScreamed = 0,
6326 }
6327
6328 if !agent.combat_state.initialized {
6329 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6330 self.health.map_or(0.0, |h| h.maximum());
6331 agent.combat_state.initialized = true;
6332 }
6333
6334 if !agent.combat_state.conditions[ActionStateConditions::ConditionHasScreamed as usize] {
6335 if self.health.is_some_and(|h| {
6338 h.current()
6339 < agent.combat_state.counters
6340 [ActionStateFCounters::FCounterHealthThreshold as usize]
6341 }) || attack_data.dist_sqrd < SCREAM_RANGE.powi(2)
6342 {
6343 agent.combat_state.conditions
6344 [ActionStateConditions::ConditionHasScreamed as usize] = true;
6345 controller.push_basic_input(InputKind::Secondary);
6346 }
6347 } else {
6348 if attack_data.in_min_range() {
6350 controller.push_basic_input(InputKind::Primary);
6351 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
6352 && entities_have_line_of_sight(
6353 self.pos,
6354 self.body,
6355 self.scale,
6356 tgt_data.pos,
6357 tgt_data.body,
6358 tgt_data.scale,
6359 read_data,
6360 )
6361 {
6362 self.path_toward_target(
6364 agent,
6365 controller,
6366 tgt_data.pos.0,
6367 read_data,
6368 Path::Partial,
6369 None,
6370 );
6371 } else {
6372 agent.combat_state.conditions
6374 [ActionStateConditions::ConditionHasScreamed as usize] = false;
6375 agent.combat_state.counters
6376 [ActionStateFCounters::FCounterHealthThreshold as usize] =
6377 self.health.map_or(0.0, |h| h.maximum());
6378 }
6379 }
6380 }
6381
6382 pub fn handle_wood_golem(
6383 &self,
6384 agent: &mut Agent,
6385 controller: &mut Controller,
6386 attack_data: &AttackData,
6387 tgt_data: &TargetData,
6388 read_data: &ReadData,
6389 rng: &mut impl Rng,
6390 ) {
6391 const PATH_RANGE_FACTOR: f32 = 0.3; const STRIKE_RANGE_FACTOR: f32 = 0.6; const STRIKE_AIM_FACTOR: f32 = 0.7;
6406 const SPIN_RANGE_FACTOR: f32 = 0.6;
6407 const SPIN_COOLDOWN: f32 = 1.5;
6408 const SPIN_RELAX_FACTOR: f32 = 0.2;
6409 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.7;
6410 const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
6411 const SHOCKWAVE_COOLDOWN: f32 = 5.0;
6412 const MIXUP_COOLDOWN: f32 = 2.5;
6413 const MIXUP_RELAX_FACTOR: f32 = 0.3;
6414
6415 const SPIN: usize = 0;
6417 const SHOCKWAVE: usize = 1;
6418 const MIXUP: usize = 2;
6419
6420 let shockwave_min_range = self.body.map_or(0.0, |b| b.height() * 1.1);
6423
6424 let (strike_range, strike_angle) = {
6426 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
6427 self.extract_ability(AbilityInput::Primary)
6428 {
6429 (range, angle)
6430 } else {
6431 (0.0, 0.0)
6432 }
6433 };
6434 let spin_range = {
6435 if let Some(AbilityData::BasicMelee { range, .. }) =
6436 self.extract_ability(AbilityInput::Secondary)
6437 {
6438 range
6439 } else {
6440 0.0
6441 }
6442 };
6443 let (shockwave_max_range, shockwave_angle) = {
6444 if let Some(AbilityData::Shockwave { range, angle, .. }) =
6445 self.extract_ability(AbilityInput::Auxiliary(0))
6446 {
6447 (range, angle)
6448 } else {
6449 (0.0, 0.0)
6450 }
6451 };
6452
6453 let is_in_spin_range = attack_data.dist_sqrd
6455 < (attack_data.body_dist + spin_range * SPIN_RANGE_FACTOR).powi(2);
6456 let is_in_strike_range = attack_data.dist_sqrd
6457 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
6458 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
6459
6460 let current_input = self.char_state.ability_info().map(|ai| ai.input);
6465 if matches!(current_input, Some(InputKind::Secondary)) {
6466 agent.combat_state.timers[SPIN] = 0.0;
6468 agent.combat_state.timers[MIXUP] = 0.0;
6469 } else if is_in_spin_range && !(is_in_strike_range && is_in_strike_angle) {
6470 agent.combat_state.timers[SPIN] += read_data.dt.0;
6472 } else {
6473 agent.combat_state.timers[SPIN] =
6475 (agent.combat_state.timers[SPIN] - read_data.dt.0 * SPIN_RELAX_FACTOR).max(0.0);
6476 }
6477 if matches!(self.char_state, CharacterState::Shockwave(_)) {
6479 agent.combat_state.timers[SHOCKWAVE] = 0.0;
6481 agent.combat_state.timers[MIXUP] = 0.0;
6482 } else {
6483 agent.combat_state.timers[SHOCKWAVE] += read_data.dt.0;
6485 }
6486 if is_in_strike_range && is_in_strike_angle {
6488 agent.combat_state.timers[MIXUP] += read_data.dt.0;
6490 } else {
6491 agent.combat_state.timers[MIXUP] =
6493 (agent.combat_state.timers[MIXUP] - read_data.dt.0 * MIXUP_RELAX_FACTOR).max(0.0);
6494 }
6495
6496 if is_in_strike_range && is_in_strike_angle {
6499 if agent.combat_state.timers[MIXUP] > MIXUP_COOLDOWN {
6501 let randomise: u8 = rng.gen_range(1..=3);
6502 match randomise {
6503 1 => controller.push_basic_input(InputKind::Ability(0)), 2 => controller.push_basic_input(InputKind::Primary), _ => controller.push_basic_input(InputKind::Secondary), }
6507 }
6508 else {
6510 controller.push_basic_input(InputKind::Primary);
6511 }
6512 }
6513 else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
6515 if agent.combat_state.timers[SPIN] > SPIN_COOLDOWN {
6517 controller.push_basic_input(InputKind::Secondary);
6518 }
6519 }
6521 else if attack_data.dist_sqrd > shockwave_min_range.powi(2)
6523 && attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
6524 && attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
6525 {
6526 if agent.combat_state.timers[SHOCKWAVE] > SHOCKWAVE_COOLDOWN {
6528 controller.push_basic_input(InputKind::Ability(0));
6529 }
6530 }
6532
6533 if attack_data.dist_sqrd
6536 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
6537 {
6538 self.path_toward_target(
6539 agent,
6540 controller,
6541 tgt_data.pos.0,
6542 read_data,
6543 Path::Partial,
6544 None,
6545 );
6546 }
6547 else if attack_data.angle > 0.0 {
6549 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6551 .xy()
6552 .try_normalized()
6553 .unwrap_or_else(Vec2::zero)
6554 * 0.001; }
6556 }
6557
6558 pub fn handle_gnarling_chieftain(
6559 &self,
6560 agent: &mut Agent,
6561 controller: &mut Controller,
6562 attack_data: &AttackData,
6563 tgt_data: &TargetData,
6564 read_data: &ReadData,
6565 rng: &mut impl Rng,
6566 ) {
6567 const PATH_RANGE_FACTOR: f32 = 0.4;
6582 const STRIKE_RANGE_FACTOR: f32 = 0.7;
6583 const STRIKE_AIM_FACTOR: f32 = 0.8;
6584 const BARRAGE_RANGE_FACTOR: f32 = 0.8;
6585 const BARRAGE_AIM_FACTOR: f32 = 0.65;
6586 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
6587 const TOTEM_COOLDOWN: f32 = 25.0;
6588 const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
6589 const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
6590 const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
6591
6592 const HAS_SUMMONED_FIRST_TOTEM: usize = 0;
6594 const SUMMON_TOTEM: usize = 0;
6596 const HEAVY_ATTACK: usize = 1;
6597 const HEAVY_ATTACK_COOLDOWN: usize = 0;
6599
6600 let line_of_sight_with_target = || {
6602 entities_have_line_of_sight(
6603 self.pos,
6604 self.body,
6605 self.scale,
6606 tgt_data.pos,
6607 tgt_data.body,
6608 tgt_data.scale,
6609 read_data,
6610 )
6611 };
6612
6613 let (strike_range, strike_angle) = {
6616 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
6617 self.extract_ability(AbilityInput::Primary)
6618 {
6619 (range, angle)
6620 } else {
6621 (0.0, 0.0)
6622 }
6623 };
6624 let (barrage_speed, barrage_spread, barrage_count) = {
6625 if let Some(AbilityData::BasicRanged {
6626 projectile_speed,
6627 projectile_spread,
6628 num_projectiles,
6629 ..
6630 }) = self.extract_ability(AbilityInput::Secondary)
6631 {
6632 (
6633 projectile_speed,
6634 projectile_spread,
6635 num_projectiles.compute(self.heads.map_or(1, |heads| heads.amount() as u32)),
6636 )
6637 } else {
6638 (0.0, 0.0, 0)
6639 }
6640 };
6641 let shockwave_range = {
6642 if let Some(AbilityData::Shockwave { range, .. }) =
6643 self.extract_ability(AbilityInput::Auxiliary(0))
6644 {
6645 range
6646 } else {
6647 0.0
6648 }
6649 };
6650
6651 let barrage_max_range =
6653 projectile_flat_range(barrage_speed, self.body.map_or(2.0, |b| b.height()));
6654 let barrange_angle = projectile_multi_angle(barrage_spread, barrage_count);
6655
6656 let is_in_strike_range = attack_data.dist_sqrd
6658 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
6659 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
6660
6661 if !agent.combat_state.initialized {
6663 agent.combat_state.initialized = true;
6664 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
6665 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
6666 }
6667
6668 match self.char_state {
6673 CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
6674 agent.combat_state.timers[SUMMON_TOTEM] = 0.0;
6676 agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] = true;
6677 },
6678 CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
6679 agent.combat_state.counters[HEAVY_ATTACK] = 0.0;
6681 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
6682 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
6683 },
6684 _ => {},
6685 }
6686 agent.combat_state.timers[SUMMON_TOTEM] += read_data.dt.0;
6688 if is_in_strike_range {
6690 if is_in_strike_angle {
6692 agent.combat_state.counters[HEAVY_ATTACK] += read_data.dt.0;
6693 } else {
6694 agent.combat_state.counters[HEAVY_ATTACK] +=
6696 read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
6697 }
6698 } else {
6699 agent.combat_state.counters[HEAVY_ATTACK] +=
6701 read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
6702 }
6703
6704 if !agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] {
6707 controller.push_basic_input(InputKind::Ability(2));
6708 }
6709 else if agent.combat_state.timers[SUMMON_TOTEM] > TOTEM_COOLDOWN {
6711 controller.push_basic_input(InputKind::Ability(rng.gen_range(1..=3)));
6712 }
6713 else if agent.combat_state.counters[HEAVY_ATTACK]
6717 > agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN]
6718 && attack_data.dist_sqrd < (barrage_max_range * BARRAGE_RANGE_FACTOR).powi(2)
6719 {
6720 if line_of_sight_with_target() {
6722 if attack_data.angle > barrange_angle * BARRAGE_AIM_FACTOR {
6724 controller.push_basic_input(InputKind::Ability(0));
6725 }
6726 else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
6728 {
6729 if rng.gen_bool(0.5) {
6730 controller.push_basic_input(InputKind::Secondary);
6731 } else {
6732 controller.push_basic_input(InputKind::Ability(0));
6733 }
6734 }
6735 else {
6737 controller.push_basic_input(InputKind::Secondary);
6738 }
6739 }
6741 else {
6743 if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
6745 controller.push_basic_input(InputKind::Ability(0));
6746 }
6747 }
6749 }
6750 else if is_in_strike_range && is_in_strike_angle {
6752 controller.push_basic_input(InputKind::Primary);
6753 }
6754 if attack_data.dist_sqrd
6759 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
6760 {
6761 self.path_toward_target(
6762 agent,
6763 controller,
6764 tgt_data.pos.0,
6765 read_data,
6766 Path::Full,
6767 None,
6768 );
6769 }
6770 else if attack_data.angle > 0.0 {
6772 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6774 .xy()
6775 .try_normalized()
6776 .unwrap_or_else(Vec2::zero)
6777 * 0.001; }
6779 }
6780
6781 pub fn handle_sword_simple_attack(
6782 &self,
6783 agent: &mut Agent,
6784 controller: &mut Controller,
6785 attack_data: &AttackData,
6786 tgt_data: &TargetData,
6787 read_data: &ReadData,
6788 ) {
6789 const DASH_TIMER: usize = 0;
6790 agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
6791 if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
6792 {
6793 controller.push_basic_input(InputKind::Secondary);
6794 } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
6795 if agent.combat_state.timers[DASH_TIMER] > 2.0 {
6796 agent.combat_state.timers[DASH_TIMER] = 0.0;
6797 }
6798 controller.push_basic_input(InputKind::Primary);
6799 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
6800 && self
6801 .path_toward_target(
6802 agent,
6803 controller,
6804 tgt_data.pos.0,
6805 read_data,
6806 Path::Separate,
6807 None,
6808 )
6809 .is_some()
6810 && entities_have_line_of_sight(
6811 self.pos,
6812 self.body,
6813 self.scale,
6814 tgt_data.pos,
6815 tgt_data.body,
6816 tgt_data.scale,
6817 read_data,
6818 )
6819 && agent.combat_state.timers[DASH_TIMER] > 4.0
6820 && attack_data.angle < 45.0
6821 {
6822 controller.push_basic_input(InputKind::Secondary);
6823 agent.combat_state.timers[DASH_TIMER] = 0.0;
6824 } else {
6825 self.path_toward_target(
6826 agent,
6827 controller,
6828 tgt_data.pos.0,
6829 read_data,
6830 Path::Partial,
6831 None,
6832 );
6833 }
6834 }
6835
6836 pub fn handle_adlet_hunter(
6837 &self,
6838 agent: &mut Agent,
6839 controller: &mut Controller,
6840 attack_data: &AttackData,
6841 tgt_data: &TargetData,
6842 read_data: &ReadData,
6843 rng: &mut impl Rng,
6844 ) {
6845 const ROTATE_TIMER: usize = 0;
6846 const ROTATE_DIR_CONDITION: usize = 0;
6847 agent.combat_state.timers[ROTATE_TIMER] -= read_data.dt.0;
6848 if agent.combat_state.timers[ROTATE_TIMER] < 0.0 {
6849 agent.combat_state.conditions[ROTATE_DIR_CONDITION] = rng.gen_bool(0.5);
6850 agent.combat_state.timers[ROTATE_TIMER] = rng.gen::<f32>() * 5.0;
6851 }
6852 let primary = self.extract_ability(AbilityInput::Primary);
6853 let secondary = self.extract_ability(AbilityInput::Secondary);
6854 let could_use_input = |input| match input {
6855 InputKind::Primary => primary.as_ref().is_some_and(|p| {
6856 p.could_use(
6857 attack_data,
6858 self,
6859 tgt_data,
6860 read_data,
6861 AbilityPreferences::default(),
6862 )
6863 }),
6864 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
6865 s.could_use(
6866 attack_data,
6867 self,
6868 tgt_data,
6869 read_data,
6870 AbilityPreferences::default(),
6871 )
6872 }),
6873 _ => false,
6874 };
6875 let move_forwards = if could_use_input(InputKind::Primary) {
6876 controller.push_basic_input(InputKind::Primary);
6877 false
6878 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 8_f32.powi(2) {
6879 controller.push_basic_input(InputKind::Secondary);
6880 true
6881 } else {
6882 true
6883 };
6884
6885 if move_forwards && attack_data.dist_sqrd > 3_f32.powi(2) {
6886 self.path_toward_target(
6887 agent,
6888 controller,
6889 tgt_data.pos.0,
6890 read_data,
6891 Path::Separate,
6892 None,
6893 );
6894 } else {
6895 self.path_toward_target(
6896 agent,
6897 controller,
6898 tgt_data.pos.0,
6899 read_data,
6900 Path::Separate,
6901 None,
6902 );
6903 let dir = if agent.combat_state.conditions[ROTATE_DIR_CONDITION] {
6904 1.0
6905 } else {
6906 -1.0
6907 };
6908 controller.inputs.move_dir.rotate_z(PI / 2.0 * dir);
6909 }
6910 }
6911
6912 pub fn handle_adlet_icepicker(
6913 &self,
6914 agent: &mut Agent,
6915 controller: &mut Controller,
6916 attack_data: &AttackData,
6917 tgt_data: &TargetData,
6918 read_data: &ReadData,
6919 ) {
6920 let primary = self.extract_ability(AbilityInput::Primary);
6921 let secondary = self.extract_ability(AbilityInput::Secondary);
6922 let could_use_input = |input| match input {
6923 InputKind::Primary => primary.as_ref().is_some_and(|p| {
6924 p.could_use(
6925 attack_data,
6926 self,
6927 tgt_data,
6928 read_data,
6929 AbilityPreferences::default(),
6930 )
6931 }),
6932 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
6933 s.could_use(
6934 attack_data,
6935 self,
6936 tgt_data,
6937 read_data,
6938 AbilityPreferences::default(),
6939 )
6940 }),
6941 _ => false,
6942 };
6943 let move_forwards = if could_use_input(InputKind::Primary) {
6944 controller.push_basic_input(InputKind::Primary);
6945 false
6946 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 5_f32.powi(2) {
6947 controller.push_basic_input(InputKind::Secondary);
6948 false
6949 } else {
6950 true
6951 };
6952
6953 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
6954 self.path_toward_target(
6955 agent,
6956 controller,
6957 tgt_data.pos.0,
6958 read_data,
6959 Path::Separate,
6960 None,
6961 );
6962 }
6963 }
6964
6965 pub fn handle_adlet_tracker(
6966 &self,
6967 agent: &mut Agent,
6968 controller: &mut Controller,
6969 attack_data: &AttackData,
6970 tgt_data: &TargetData,
6971 read_data: &ReadData,
6972 ) {
6973 const TRAP_TIMER: usize = 0;
6974 agent.combat_state.timers[TRAP_TIMER] += read_data.dt.0;
6975 if agent.combat_state.timers[TRAP_TIMER] > 20.0 {
6976 agent.combat_state.timers[TRAP_TIMER] = 0.0;
6977 }
6978 let primary = self.extract_ability(AbilityInput::Primary);
6979 let could_use_input = |input| match input {
6980 InputKind::Primary => primary.as_ref().is_some_and(|p| {
6981 p.could_use(
6982 attack_data,
6983 self,
6984 tgt_data,
6985 read_data,
6986 AbilityPreferences::default(),
6987 )
6988 }),
6989 _ => false,
6990 };
6991 let move_forwards = if agent.combat_state.timers[TRAP_TIMER] < 3.0 {
6992 controller.push_basic_input(InputKind::Secondary);
6993 false
6994 } else if could_use_input(InputKind::Primary) {
6995 controller.push_basic_input(InputKind::Primary);
6996 false
6997 } else {
6998 true
6999 };
7000
7001 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7002 self.path_toward_target(
7003 agent,
7004 controller,
7005 tgt_data.pos.0,
7006 read_data,
7007 Path::Separate,
7008 None,
7009 );
7010 }
7011 }
7012
7013 pub fn handle_adlet_elder(
7014 &self,
7015 agent: &mut Agent,
7016 controller: &mut Controller,
7017 attack_data: &AttackData,
7018 tgt_data: &TargetData,
7019 read_data: &ReadData,
7020 rng: &mut impl Rng,
7021 ) {
7022 const TRAP_TIMER: usize = 0;
7023 agent.combat_state.timers[TRAP_TIMER] -= read_data.dt.0;
7024 if matches!(self.char_state, CharacterState::BasicRanged(_)) {
7025 agent.combat_state.timers[TRAP_TIMER] = 15.0;
7026 }
7027 let primary = self.extract_ability(AbilityInput::Primary);
7028 let secondary = self.extract_ability(AbilityInput::Secondary);
7029 let abilities = [
7030 self.extract_ability(AbilityInput::Auxiliary(0)),
7031 self.extract_ability(AbilityInput::Auxiliary(1)),
7032 ];
7033 let could_use_input = |input| match input {
7034 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7035 p.could_use(
7036 attack_data,
7037 self,
7038 tgt_data,
7039 read_data,
7040 AbilityPreferences::default(),
7041 )
7042 }),
7043 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7044 s.could_use(
7045 attack_data,
7046 self,
7047 tgt_data,
7048 read_data,
7049 AbilityPreferences::default(),
7050 )
7051 }),
7052 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7053 a.could_use(
7054 attack_data,
7055 self,
7056 tgt_data,
7057 read_data,
7058 AbilityPreferences::default(),
7059 )
7060 }),
7061 _ => false,
7062 };
7063 let move_forwards = if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
7064 {
7065 controller.push_basic_input(InputKind::Secondary);
7066 false
7067 } else if agent.combat_state.timers[TRAP_TIMER] < 0.0 && !tgt_data.considered_ranged() {
7068 controller.push_basic_input(InputKind::Ability(0));
7069 false
7070 } else if could_use_input(InputKind::Primary) {
7071 controller.push_basic_input(InputKind::Primary);
7072 false
7073 } else if could_use_input(InputKind::Secondary) && rng.gen_bool(0.5) {
7074 controller.push_basic_input(InputKind::Secondary);
7075 false
7076 } else if could_use_input(InputKind::Ability(1)) {
7077 controller.push_basic_input(InputKind::Ability(1));
7078 false
7079 } else {
7080 true
7081 };
7082
7083 if matches!(self.char_state, CharacterState::LeapMelee(_)) {
7084 let tgt_vec = tgt_data.pos.0.xy() - self.pos.0.xy();
7085 if tgt_vec.magnitude_squared() > 2_f32.powi(2) {
7086 if let Some(look_dir) = Dir::from_unnormalized(Vec3::from(tgt_vec)) {
7087 controller.inputs.look_dir = look_dir;
7088 }
7089 }
7090 }
7091
7092 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7093 self.path_toward_target(
7094 agent,
7095 controller,
7096 tgt_data.pos.0,
7097 read_data,
7098 Path::Separate,
7099 None,
7100 );
7101 }
7102 }
7103
7104 pub fn handle_icedrake(
7105 &self,
7106 agent: &mut Agent,
7107 controller: &mut Controller,
7108 attack_data: &AttackData,
7109 tgt_data: &TargetData,
7110 read_data: &ReadData,
7111 rng: &mut impl Rng,
7112 ) {
7113 let primary = self.extract_ability(AbilityInput::Primary);
7114 let secondary = self.extract_ability(AbilityInput::Secondary);
7115 let abilities = [
7116 self.extract_ability(AbilityInput::Auxiliary(0)),
7117 self.extract_ability(AbilityInput::Auxiliary(1)),
7118 ];
7119 let could_use_input = |input| match input {
7120 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7121 p.could_use(
7122 attack_data,
7123 self,
7124 tgt_data,
7125 read_data,
7126 AbilityPreferences::default(),
7127 )
7128 }),
7129 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7130 s.could_use(
7131 attack_data,
7132 self,
7133 tgt_data,
7134 read_data,
7135 AbilityPreferences::default(),
7136 )
7137 }),
7138 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7139 a.could_use(
7140 attack_data,
7141 self,
7142 tgt_data,
7143 read_data,
7144 AbilityPreferences::default(),
7145 )
7146 }),
7147 _ => false,
7148 };
7149
7150 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7151 Some(input @ InputKind::Primary) => {
7152 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7153 && could_use_input(input)
7154 {
7155 controller.push_basic_input(input);
7156 true
7157 } else {
7158 false
7159 }
7160 },
7161 Some(input @ InputKind::Ability(1)) => {
7162 if self
7163 .char_state
7164 .timer()
7165 .is_some_and(|t| t.as_secs_f32() < 3.0)
7166 && could_use_input(input)
7167 {
7168 controller.push_basic_input(input);
7169 true
7170 } else {
7171 false
7172 }
7173 },
7174 _ => false,
7175 };
7176
7177 let move_forwards = if !continued_attack {
7178 if could_use_input(InputKind::Primary) && rng.gen_bool(0.4) {
7179 controller.push_basic_input(InputKind::Primary);
7180 false
7181 } else if could_use_input(InputKind::Secondary) && rng.gen_bool(0.8) {
7182 controller.push_basic_input(InputKind::Secondary);
7183 false
7184 } else if could_use_input(InputKind::Ability(1)) && rng.gen_bool(0.9) {
7185 controller.push_basic_input(InputKind::Ability(1));
7186 true
7187 } else if could_use_input(InputKind::Ability(0)) {
7188 controller.push_basic_input(InputKind::Ability(0));
7189 true
7190 } else {
7191 true
7192 }
7193 } else {
7194 false
7195 };
7196
7197 if move_forwards {
7198 self.path_toward_target(
7199 agent,
7200 controller,
7201 tgt_data.pos.0,
7202 read_data,
7203 Path::Separate,
7204 None,
7205 );
7206 }
7207 }
7208
7209 pub fn handle_hydra(
7210 &self,
7211 agent: &mut Agent,
7212 controller: &mut Controller,
7213 attack_data: &AttackData,
7214 tgt_data: &TargetData,
7215 read_data: &ReadData,
7216 rng: &mut impl Rng,
7217 ) {
7218 enum ActionStateTimers {
7219 RegrowHeadNoDamage,
7220 RegrowHeadNoAttack,
7221 }
7222
7223 let could_use_input = |input| {
7224 Option::from(input)
7225 .and_then(|ability| {
7226 Some(self.extract_ability(ability)?.could_use(
7227 attack_data,
7228 self,
7229 tgt_data,
7230 read_data,
7231 AbilityPreferences::default(),
7232 ))
7233 })
7234 .unwrap_or(false)
7235 };
7236
7237 const FOCUS_ATTACK_RANGE: f32 = 5.0;
7238
7239 if attack_data.dist_sqrd < FOCUS_ATTACK_RANGE.powi(2) {
7240 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] = 0.0;
7241 } else {
7242 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] +=
7243 read_data.dt.0;
7244 }
7245
7246 if let Some(health) = self.health.filter(|health| health.last_change.amount < 0.0) {
7247 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] =
7248 (read_data.time.0 - health.last_change.time.0) as f32;
7249 } else {
7250 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] +=
7251 read_data.dt.0;
7252 }
7253
7254 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
7255 match self.char_state {
7256 CharacterState::ChargedMelee(c) => {
7257 if c.charge_frac() < 1.0 && could_use_input(input) {
7258 controller.push_basic_input(input);
7259 }
7260 },
7261 CharacterState::ChargedRanged(c) => {
7262 if c.charge_frac() < 1.0 && could_use_input(input) {
7263 controller.push_basic_input(input);
7264 }
7265 },
7266 _ => {},
7267 }
7268 }
7269
7270 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7271 Some(input @ InputKind::Primary) => {
7272 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7273 && could_use_input(input)
7274 {
7275 controller.push_basic_input(input);
7276 true
7277 } else {
7278 false
7279 }
7280 },
7281 _ => false,
7282 };
7283
7284 let has_heads = self.heads.is_none_or(|heads| heads.amount() > 0);
7285
7286 let move_forwards = if !continued_attack {
7287 if could_use_input(InputKind::Ability(1))
7288 && rng.gen_bool(0.9)
7289 && (agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] > 5.0
7290 || agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize]
7291 > 6.0)
7292 && self.heads.is_some_and(|heads| heads.amount_missing() > 0)
7293 {
7294 controller.push_basic_input(InputKind::Ability(2));
7295 false
7296 } else if has_heads && could_use_input(InputKind::Primary) && rng.gen_bool(0.8) {
7297 controller.push_basic_input(InputKind::Primary);
7298 true
7299 } else if has_heads && could_use_input(InputKind::Secondary) && rng.gen_bool(0.4) {
7300 controller.push_basic_input(InputKind::Secondary);
7301 false
7302 } else if has_heads && could_use_input(InputKind::Ability(1)) && rng.gen_bool(0.6) {
7303 controller.push_basic_input(InputKind::Ability(1));
7304 true
7305 } else if !has_heads && could_use_input(InputKind::Ability(3)) && rng.gen_bool(0.7) {
7306 controller.push_basic_input(InputKind::Ability(3));
7307 true
7308 } else if could_use_input(InputKind::Ability(0)) {
7309 controller.push_basic_input(InputKind::Ability(0));
7310 true
7311 } else {
7312 true
7313 }
7314 } else {
7315 true
7316 };
7317
7318 if move_forwards {
7319 if has_heads {
7320 self.path_toward_target(
7321 agent,
7322 controller,
7323 tgt_data.pos.0,
7324 read_data,
7325 Path::Separate,
7326 (attack_data.dist_sqrd
7328 < (2.5 + self.body.map_or(0.0, |b| b.front_radius())).powi(2))
7329 .then_some(0.3),
7330 );
7331 } else {
7332 self.flee(agent, controller, read_data, tgt_data.pos);
7333 }
7334 }
7335 }
7336
7337 pub fn handle_random_abilities(
7338 &self,
7339 agent: &mut Agent,
7340 controller: &mut Controller,
7341 attack_data: &AttackData,
7342 tgt_data: &TargetData,
7343 read_data: &ReadData,
7344 rng: &mut impl Rng,
7345 primary_weight: u8,
7346 secondary_weight: u8,
7347 ability_weights: [u8; BASE_ABILITY_LIMIT],
7348 ) {
7349 let primary = self.extract_ability(AbilityInput::Primary);
7350 let secondary = self.extract_ability(AbilityInput::Secondary);
7351 let abilities = [
7352 self.extract_ability(AbilityInput::Auxiliary(0)),
7353 self.extract_ability(AbilityInput::Auxiliary(1)),
7354 self.extract_ability(AbilityInput::Auxiliary(2)),
7355 self.extract_ability(AbilityInput::Auxiliary(3)),
7356 self.extract_ability(AbilityInput::Auxiliary(4)),
7357 ];
7358 let could_use_input = |input| match input {
7359 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7360 p.could_use(
7361 attack_data,
7362 self,
7363 tgt_data,
7364 read_data,
7365 AbilityPreferences::default(),
7366 )
7367 }),
7368 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7369 s.could_use(
7370 attack_data,
7371 self,
7372 tgt_data,
7373 read_data,
7374 AbilityPreferences::default(),
7375 )
7376 }),
7377 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7378 a.could_use(
7379 attack_data,
7380 self,
7381 tgt_data,
7382 read_data,
7383 AbilityPreferences::default(),
7384 )
7385 }),
7386 _ => false,
7387 };
7388
7389 let primary_chance = primary_weight as f64
7390 / ((primary_weight + secondary_weight + ability_weights.iter().sum::<u8>()) as f64)
7391 .max(0.01);
7392 let secondary_chance = secondary_weight as f64
7393 / ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
7394 let ability_chances = {
7395 let mut chances = [0.0; BASE_ABILITY_LIMIT];
7396 chances.iter_mut().enumerate().for_each(|(i, chance)| {
7397 *chance = ability_weights[i] as f64
7398 / (ability_weights
7399 .iter()
7400 .enumerate()
7401 .filter_map(|(j, weight)| if j >= i { Some(weight) } else { None })
7402 .sum::<u8>() as f64)
7403 .max(0.01)
7404 });
7405 chances
7406 };
7407
7408 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
7409 match self.char_state {
7410 CharacterState::ChargedMelee(c) => {
7411 if c.charge_frac() < 1.0 && could_use_input(input) {
7412 controller.push_basic_input(input);
7413 }
7414 },
7415 CharacterState::ChargedRanged(c) => {
7416 if c.charge_frac() < 1.0 && could_use_input(input) {
7417 controller.push_basic_input(input);
7418 }
7419 },
7420 _ => {},
7421 }
7422 }
7423
7424 let move_forwards = if could_use_input(InputKind::Primary) && rng.gen_bool(primary_chance) {
7425 controller.push_basic_input(InputKind::Primary);
7426 false
7427 } else if could_use_input(InputKind::Secondary) && rng.gen_bool(secondary_chance) {
7428 controller.push_basic_input(InputKind::Secondary);
7429 false
7430 } else if could_use_input(InputKind::Ability(0)) && rng.gen_bool(ability_chances[0]) {
7431 controller.push_basic_input(InputKind::Ability(0));
7432 false
7433 } else if could_use_input(InputKind::Ability(1)) && rng.gen_bool(ability_chances[1]) {
7434 controller.push_basic_input(InputKind::Ability(1));
7435 false
7436 } else if could_use_input(InputKind::Ability(2)) && rng.gen_bool(ability_chances[2]) {
7437 controller.push_basic_input(InputKind::Ability(2));
7438 false
7439 } else if could_use_input(InputKind::Ability(3)) && rng.gen_bool(ability_chances[3]) {
7440 controller.push_basic_input(InputKind::Ability(3));
7441 false
7442 } else if could_use_input(InputKind::Ability(4)) && rng.gen_bool(ability_chances[4]) {
7443 controller.push_basic_input(InputKind::Ability(4));
7444 false
7445 } else {
7446 true
7447 };
7448
7449 if move_forwards {
7450 self.path_toward_target(
7451 agent,
7452 controller,
7453 tgt_data.pos.0,
7454 read_data,
7455 Path::Separate,
7456 None,
7457 );
7458 }
7459 }
7460
7461 pub fn handle_simple_double_attack(
7462 &self,
7463 agent: &mut Agent,
7464 controller: &mut Controller,
7465 attack_data: &AttackData,
7466 tgt_data: &TargetData,
7467 read_data: &ReadData,
7468 ) {
7469 const MAX_ATTACK_RANGE: f32 = 20.0;
7470
7471 if attack_data.angle < 60.0 && attack_data.dist_sqrd < MAX_ATTACK_RANGE.powi(2) {
7472 controller.inputs.move_dir = Vec2::zero();
7473 if attack_data.in_min_range() {
7474 controller.push_basic_input(InputKind::Primary);
7475 } else {
7476 controller.push_basic_input(InputKind::Secondary);
7477 }
7478 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
7479 self.path_toward_target(
7480 agent,
7481 controller,
7482 tgt_data.pos.0,
7483 read_data,
7484 Path::Separate,
7485 None,
7486 );
7487 } else {
7488 self.path_toward_target(
7489 agent,
7490 controller,
7491 tgt_data.pos.0,
7492 read_data,
7493 Path::Partial,
7494 None,
7495 );
7496 }
7497 }
7498
7499 pub fn handle_clay_steed_attack(
7500 &self,
7501 agent: &mut Agent,
7502 controller: &mut Controller,
7503 attack_data: &AttackData,
7504 tgt_data: &TargetData,
7505 read_data: &ReadData,
7506 ) {
7507 enum ActionStateTimers {
7508 AttackTimer,
7509 }
7510 const HOOF_ATTACK_RANGE: f32 = 1.0;
7511 const HOOF_ATTACK_ANGLE: f32 = 50.0;
7512
7513 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
7514 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 10.0 {
7515 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
7517 }
7518
7519 if attack_data.angle < HOOF_ATTACK_ANGLE
7520 && attack_data.dist_sqrd
7521 < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.max_radius())).powi(2)
7522 {
7523 controller.inputs.move_dir = Vec2::zero();
7524 controller.push_basic_input(InputKind::Primary);
7525 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
7526 controller.push_basic_input(InputKind::Secondary);
7527 } else {
7528 self.path_toward_target(
7529 agent,
7530 controller,
7531 tgt_data.pos.0,
7532 read_data,
7533 Path::Full,
7534 None,
7535 );
7536 }
7537 }
7538
7539 pub fn handle_ancient_effigy_attack(
7540 &self,
7541 agent: &mut Agent,
7542 controller: &mut Controller,
7543 attack_data: &AttackData,
7544 tgt_data: &TargetData,
7545 read_data: &ReadData,
7546 ) {
7547 enum ActionStateTimers {
7548 BlastTimer,
7549 }
7550
7551 let home = agent.patrol_origin.unwrap_or(self.pos.0);
7552 let line_of_sight_with_target = || {
7553 entities_have_line_of_sight(
7554 self.pos,
7555 self.body,
7556 self.scale,
7557 tgt_data.pos,
7558 tgt_data.body,
7559 tgt_data.scale,
7560 read_data,
7561 )
7562 };
7563 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] += read_data.dt.0;
7564
7565 if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] > 6.0 {
7566 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] = 0.0;
7567 }
7568 if line_of_sight_with_target() {
7569 if attack_data.in_min_range() {
7570 controller.push_basic_input(InputKind::Secondary);
7571 } else if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] < 2.0 {
7572 controller.push_basic_input(InputKind::Primary);
7573 } else {
7574 self.path_toward_target(
7575 agent,
7576 controller,
7577 tgt_data.pos.0,
7578 read_data,
7579 Path::Separate,
7580 None,
7581 );
7582 }
7583 } else {
7584 if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
7586 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
7587 }
7588 }
7589 }
7590
7591 pub fn handle_clay_golem_attack(
7592 &self,
7593 agent: &mut Agent,
7594 controller: &mut Controller,
7595 attack_data: &AttackData,
7596 tgt_data: &TargetData,
7597 read_data: &ReadData,
7598 ) {
7599 const MIN_DASH_RANGE: f32 = 15.0;
7600
7601 enum ActionStateTimers {
7602 AttackTimer,
7603 }
7604
7605 let line_of_sight_with_target = || {
7606 entities_have_line_of_sight(
7607 self.pos,
7608 self.body,
7609 self.scale,
7610 tgt_data.pos,
7611 tgt_data.body,
7612 tgt_data.scale,
7613 read_data,
7614 )
7615 };
7616 let spawn = agent.patrol_origin.unwrap_or(self.pos.0);
7617 let home = Vec3::new(spawn.x - 32.0, spawn.y - 12.0, spawn.z);
7618 let is_home = (home - self.pos.0).xy().magnitude_squared() < (3.0_f32).powi(2);
7619 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
7620 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
7621 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
7623 }
7624 if line_of_sight_with_target() {
7625 controller.inputs.move_dir = Vec2::zero();
7626 if attack_data.in_min_range() {
7627 controller.push_basic_input(InputKind::Primary);
7628 } else if attack_data.dist_sqrd > MIN_DASH_RANGE.powi(2) {
7629 controller.push_basic_input(InputKind::Secondary);
7630 } else {
7631 self.path_toward_target(
7632 agent,
7633 controller,
7634 tgt_data.pos.0,
7635 read_data,
7636 Path::Partial,
7637 None,
7638 );
7639 }
7640 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.0 {
7641 if !is_home {
7642 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
7644 } else {
7645 self.path_toward_target(agent, controller, spawn, read_data, Path::Separate, None);
7646 }
7647 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
7648 self.path_toward_target(
7649 agent,
7650 controller,
7651 tgt_data.pos.0,
7652 read_data,
7653 Path::Separate,
7654 None,
7655 );
7656 }
7657 }
7658
7659 pub fn handle_haniwa_soldier(
7660 &self,
7661 agent: &mut Agent,
7662 controller: &mut Controller,
7663 attack_data: &AttackData,
7664 tgt_data: &TargetData,
7665 read_data: &ReadData,
7666 ) {
7667 const DEFENSIVE_CONDITION: usize = 0;
7668 const RIPOSTE_TIMER: usize = 0;
7669 const MODE_CYCLE_TIMER: usize = 1;
7670
7671 let primary = self.extract_ability(AbilityInput::Primary);
7672 let secondary = self.extract_ability(AbilityInput::Secondary);
7673 let could_use_input = |input| match input {
7674 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7675 p.could_use(
7676 attack_data,
7677 self,
7678 tgt_data,
7679 read_data,
7680 AbilityPreferences::default(),
7681 )
7682 }),
7683 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7684 s.could_use(
7685 attack_data,
7686 self,
7687 tgt_data,
7688 read_data,
7689 AbilityPreferences::default(),
7690 )
7691 }),
7692 _ => false,
7693 };
7694
7695 agent.combat_state.timers[RIPOSTE_TIMER] += read_data.dt.0;
7696 agent.combat_state.timers[MODE_CYCLE_TIMER] += read_data.dt.0;
7697
7698 if agent.combat_state.timers[MODE_CYCLE_TIMER] > 7.0 {
7699 agent.combat_state.conditions[DEFENSIVE_CONDITION] =
7700 !agent.combat_state.conditions[DEFENSIVE_CONDITION];
7701 agent.combat_state.timers[MODE_CYCLE_TIMER] = 0.0;
7702 }
7703
7704 if matches!(self.char_state, CharacterState::RiposteMelee(_)) {
7705 agent.combat_state.timers[RIPOSTE_TIMER] = 0.0;
7706 }
7707
7708 let try_move = if agent.combat_state.conditions[DEFENSIVE_CONDITION] {
7709 controller.push_basic_input(InputKind::Block);
7710 true
7711 } else if agent.combat_state.timers[RIPOSTE_TIMER] > 10.0
7712 && could_use_input(InputKind::Secondary)
7713 {
7714 controller.push_basic_input(InputKind::Secondary);
7715 false
7716 } else if could_use_input(InputKind::Primary) {
7717 controller.push_basic_input(InputKind::Primary);
7718 false
7719 } else {
7720 true
7721 };
7722
7723 if try_move && attack_data.dist_sqrd > 2_f32.powi(2) {
7724 self.path_toward_target(
7725 agent,
7726 controller,
7727 tgt_data.pos.0,
7728 read_data,
7729 Path::Separate,
7730 None,
7731 );
7732 }
7733 }
7734
7735 pub fn handle_haniwa_guard(
7736 &self,
7737 agent: &mut Agent,
7738 controller: &mut Controller,
7739 attack_data: &AttackData,
7740 tgt_data: &TargetData,
7741 read_data: &ReadData,
7742 rng: &mut impl Rng,
7743 ) {
7744 const BACKPEDAL_DIST: f32 = 5.0;
7745 const ROTATE_CCW_CONDITION: usize = 0;
7746 const FLURRY_TIMER: usize = 0;
7747 const BACKPEDAL_TIMER: usize = 1;
7748 const SWITCH_ROTATE_TIMER: usize = 2;
7749 const SWITCH_ROTATE_COUNTER: usize = 0;
7750
7751 let primary = self.extract_ability(AbilityInput::Primary);
7752 let secondary = self.extract_ability(AbilityInput::Secondary);
7753 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
7754 let could_use_input = |input| match input {
7755 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7756 p.could_use(
7757 attack_data,
7758 self,
7759 tgt_data,
7760 read_data,
7761 AbilityPreferences::default(),
7762 )
7763 }),
7764 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7765 s.could_use(
7766 attack_data,
7767 self,
7768 tgt_data,
7769 read_data,
7770 AbilityPreferences::default(),
7771 )
7772 }),
7773 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7774 a.could_use(
7775 attack_data,
7776 self,
7777 tgt_data,
7778 read_data,
7779 AbilityPreferences::default(),
7780 )
7781 }),
7782 _ => false,
7783 };
7784
7785 if !agent.combat_state.initialized {
7786 agent.combat_state.conditions[ROTATE_CCW_CONDITION] = rng.gen_bool(0.5);
7787 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.gen_range(5.0..20.0);
7788 agent.combat_state.initialized = true;
7789 }
7790
7791 let continue_flurry = match self.char_state {
7792 CharacterState::BasicMelee(_) => {
7793 agent.combat_state.timers[FLURRY_TIMER] += read_data.dt.0;
7794 false
7795 },
7796 CharacterState::RapidMelee(c) => {
7797 agent.combat_state.timers[FLURRY_TIMER] = 0.0;
7798 !matches!(c.stage_section, StageSection::Recover)
7799 },
7800 CharacterState::ComboMelee2(_) => {
7801 agent.combat_state.timers[BACKPEDAL_TIMER] = 0.0;
7802 false
7803 },
7804 _ => false,
7805 };
7806 agent.combat_state.timers[SWITCH_ROTATE_TIMER] += read_data.dt.0;
7807 agent.combat_state.timers[BACKPEDAL_TIMER] += read_data.dt.0;
7808
7809 if agent.combat_state.timers[SWITCH_ROTATE_TIMER]
7810 > agent.combat_state.counters[SWITCH_ROTATE_COUNTER]
7811 {
7812 agent.combat_state.conditions[ROTATE_CCW_CONDITION] =
7813 !agent.combat_state.conditions[ROTATE_CCW_CONDITION];
7814 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.gen_range(5.0..20.0);
7815 }
7816
7817 let move_farther = attack_data.dist_sqrd < BACKPEDAL_DIST.powi(2);
7818 let move_closer = if continue_flurry && could_use_input(InputKind::Secondary) {
7819 controller.push_basic_input(InputKind::Secondary);
7820 false
7821 } else if agent.combat_state.timers[BACKPEDAL_TIMER] > 10.0
7822 && move_farther
7823 && could_use_input(InputKind::Ability(0))
7824 {
7825 controller.push_basic_input(InputKind::Ability(0));
7826 false
7827 } else if agent.combat_state.timers[FLURRY_TIMER] > 6.0
7828 && could_use_input(InputKind::Secondary)
7829 {
7830 controller.push_basic_input(InputKind::Secondary);
7831 false
7832 } else if could_use_input(InputKind::Primary) {
7833 controller.push_basic_input(InputKind::Primary);
7834 false
7835 } else {
7836 true
7837 };
7838
7839 if let Some((bearing, speed)) = agent.chaser.chase(
7840 &*read_data.terrain,
7841 self.pos.0,
7842 self.vel.0,
7843 tgt_data.pos.0,
7844 TraversalConfig {
7845 min_tgt_dist: 1.25,
7846 ..self.traversal_config
7847 },
7848 ) {
7849 if entities_have_line_of_sight(
7850 self.pos,
7851 self.body,
7852 self.scale,
7853 tgt_data.pos,
7854 tgt_data.body,
7855 tgt_data.scale,
7856 read_data,
7857 ) && attack_data.angle < 45.0
7858 {
7859 let angle = match (
7860 agent.combat_state.conditions[ROTATE_CCW_CONDITION],
7861 move_closer,
7862 move_farther,
7863 ) {
7864 (true, true, false) => rng.gen_range(-1.5..-0.5),
7865 (true, false, true) => rng.gen_range(-2.2..-1.7),
7866 (true, _, _) => rng.gen_range(-1.7..-1.5),
7867 (false, true, false) => rng.gen_range(0.5..1.5),
7868 (false, false, true) => rng.gen_range(1.7..2.2),
7869 (false, _, _) => rng.gen_range(1.5..1.7),
7870 };
7871 controller.inputs.move_dir = bearing
7872 .xy()
7873 .rotated_z(angle)
7874 .try_normalized()
7875 .unwrap_or_else(Vec2::zero)
7876 * speed;
7877 } else {
7878 controller.inputs.move_dir =
7879 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
7880 self.jump_if(bearing.z > 1.5, controller);
7881 }
7882 }
7883 }
7884
7885 pub fn handle_haniwa_archer(
7886 &self,
7887 agent: &mut Agent,
7888 controller: &mut Controller,
7889 attack_data: &AttackData,
7890 tgt_data: &TargetData,
7891 read_data: &ReadData,
7892 ) {
7893 const KICK_TIMER: usize = 0;
7894 const EXPLOSIVE_TIMER: usize = 1;
7895
7896 let primary = self.extract_ability(AbilityInput::Primary);
7897 let secondary = self.extract_ability(AbilityInput::Secondary);
7898 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
7899 let could_use_input = |input| match input {
7900 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7901 p.could_use(
7902 attack_data,
7903 self,
7904 tgt_data,
7905 read_data,
7906 AbilityPreferences::default(),
7907 )
7908 }),
7909 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7910 s.could_use(
7911 attack_data,
7912 self,
7913 tgt_data,
7914 read_data,
7915 AbilityPreferences::default(),
7916 )
7917 }),
7918 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7919 a.could_use(
7920 attack_data,
7921 self,
7922 tgt_data,
7923 read_data,
7924 AbilityPreferences::default(),
7925 )
7926 }),
7927 _ => false,
7928 };
7929
7930 agent.combat_state.timers[KICK_TIMER] += read_data.dt.0;
7931 agent.combat_state.timers[EXPLOSIVE_TIMER] += read_data.dt.0;
7932
7933 match self.char_state.ability_info().map(|ai| ai.input) {
7934 Some(InputKind::Secondary) => {
7935 agent.combat_state.timers[KICK_TIMER] = 0.0;
7936 },
7937 Some(InputKind::Ability(0)) => {
7938 agent.combat_state.timers[EXPLOSIVE_TIMER] = 0.0;
7939 },
7940 _ => {},
7941 }
7942
7943 if agent.combat_state.timers[KICK_TIMER] > 4.0 && could_use_input(InputKind::Secondary) {
7944 controller.push_basic_input(InputKind::Secondary);
7945 } else if agent.combat_state.timers[EXPLOSIVE_TIMER] > 15.0
7946 && could_use_input(InputKind::Ability(0))
7947 {
7948 controller.push_basic_input(InputKind::Ability(0));
7949 } else if could_use_input(InputKind::Primary) {
7950 controller.push_basic_input(InputKind::Primary);
7951 } else {
7952 self.path_toward_target(
7953 agent,
7954 controller,
7955 tgt_data.pos.0,
7956 read_data,
7957 Path::Separate,
7958 None,
7959 );
7960 }
7961 }
7962
7963 pub fn handle_terracotta_statue_attack(
7964 &self,
7965 agent: &mut Agent,
7966 controller: &mut Controller,
7967 attack_data: &AttackData,
7968 read_data: &ReadData,
7969 ) {
7970 enum Conditions {
7971 AttackToggle,
7972 }
7973 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
7974 if (home - self.pos.0).xy().magnitude_squared() > (2.0_f32).powi(2) {
7976 self.path_toward_target(agent, controller, home, read_data, Path::Full, None);
7977 } else if !agent.combat_state.conditions[Conditions::AttackToggle as usize] {
7978 controller.push_basic_input(InputKind::Primary);
7980 } else {
7981 controller.inputs.move_dir = Vec2::zero();
7982 if attack_data.dist_sqrd < 8.5f32.powi(2) {
7983 controller.push_basic_input(InputKind::Primary);
7985 } else {
7986 controller.push_basic_input(InputKind::Secondary);
7988 }
7989 }
7990 if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
7991 {
7992 agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
7993 }
7994 }
7995
7996 pub fn handle_jiangshi_attack(
7997 &self,
7998 agent: &mut Agent,
7999 controller: &mut Controller,
8000 attack_data: &AttackData,
8001 tgt_data: &TargetData,
8002 read_data: &ReadData,
8003 ) {
8004 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
8005 controller.push_action(ControlAction::StartInput {
8006 input: InputKind::Secondary,
8007 target_entity: agent
8008 .target
8009 .as_ref()
8010 .and_then(|t| read_data.uids.get(t.target))
8011 .copied(),
8012 select_pos: None,
8013 });
8014 } else if attack_data.dist_sqrd < 12.0f32.powi(2) {
8015 controller.push_basic_input(InputKind::Primary);
8016 }
8017
8018 self.path_toward_target(
8019 agent,
8020 controller,
8021 tgt_data.pos.0,
8022 read_data,
8023 Path::Full,
8024 None,
8025 );
8026 }
8027}