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 = 0,
4356 CanSeeTarget = 1,
4357 }
4358
4359 let minotaur_attack_distance =
4360 self.body.map_or(0.0, |b| b.max_radius()) + MINOTAUR_ATTACK_RANGE;
4361 let health_fraction = self.health.map_or(1.0, |h| h.fraction());
4362 let home = agent.patrol_origin.unwrap_or(self.pos.0);
4363 let cheesed_from_above = tgt_data.pos.0.z > self.pos.0.z + 4.0;
4364 let pillar_cheesed = tgt_data.pos.0.z > home.z
4365 && agent.combat_state.timers[Timers::CheeseTimer as usize] > 4.0;
4366 agent.combat_state.timers[Timers::CheeseTimer as usize] += read_data.dt.0;
4367 agent.combat_state.timers[Timers::CanSeeTarget as usize] += read_data.dt.0;
4368 let line_of_sight_with_target = || {
4369 entities_have_line_of_sight(
4370 self.pos,
4371 self.body,
4372 self.scale,
4373 tgt_data.pos,
4374 tgt_data.body,
4375 tgt_data.scale,
4376 read_data,
4377 )
4378 };
4379 if !line_of_sight_with_target() {
4380 agent.combat_state.timers[Timers::CanSeeTarget as usize] = 0.0;
4381 };
4382 let remote_spikes_action = || ControlAction::StartInput {
4383 input: InputKind::Ability(3),
4384 target_entity: None,
4385 select_pos: Some(tgt_data.pos.0),
4386 };
4387 if agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4389 < MINOTAUR_FRENZY_THRESHOLD
4390 && health_fraction > MINOTAUR_FRENZY_THRESHOLD
4391 {
4392 agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize] =
4393 MINOTAUR_FRENZY_THRESHOLD;
4394 }
4395 if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
4396 {
4397 agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
4398 }
4399 if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
4400 {
4401 agent.combat_state.conditions[Conditions::AttackToggle as usize] = false;
4402 if agent.combat_state.timers[Timers::CheeseTimer as usize] > 10.0 {
4403 agent.combat_state.timers[Timers::CheeseTimer as usize] = 0.0;
4404 }
4405 }
4406 if cheesed_from_above || pillar_cheesed {
4408 if agent.combat_state.conditions[Conditions::AttackToggle as usize] {
4409 controller.push_basic_input(InputKind::Ability(2));
4410 } else {
4411 controller.push_action(remote_spikes_action());
4412 }
4413 } else if health_fraction
4414 < agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4415 {
4416 controller.push_basic_input(InputKind::Ability(1));
4418 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4419 {
4420 agent.combat_state.counters
4421 [ActionStateFCounters::FCounterMinotaurAttack as usize] = 0.0;
4422 }
4423 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4424 {
4425 controller.push_basic_input(InputKind::Ability(0));
4427 } else if matches!(self.char_state, CharacterState::ChargedMelee(c) if matches!(c.stage_section, StageSection::Charge) && c.timer < c.static_data.charge_duration)
4428 {
4429 controller.push_basic_input(InputKind::Primary);
4431 } else if attack_data.dist_sqrd > MINOTAUR_CHARGE_DISTANCE.powi(2) {
4432 if attack_data.angle < 60.0 {
4434 controller.push_basic_input(InputKind::Ability(0));
4435 }
4436 } else if attack_data.dist_sqrd < minotaur_attack_distance.powi(2) {
4437 if agent.combat_state.conditions
4438 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize]
4439 && !self.char_state.is_attack()
4440 {
4441 controller.push_basic_input(InputKind::Secondary);
4443 agent.combat_state.conditions
4444 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = false;
4445 } else if !self.char_state.is_attack() {
4446 controller.push_basic_input(InputKind::Primary);
4448 agent.combat_state.conditions
4449 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = true;
4450 }
4451 }
4452 if cheesed_from_above {
4454 self.path_toward_target(agent, controller, home, read_data, Path::Full, None);
4455 } else if agent.combat_state.timers[Timers::CanSeeTarget as usize] > 2.0
4457 || (3.0..18.0).contains(&(self.pos.0.y - home.y))
4459 {
4460 self.path_toward_target(
4461 agent,
4462 controller,
4463 tgt_data.pos.0,
4464 read_data,
4465 Path::Partial,
4466 (attack_data.dist_sqrd
4467 < (attack_data.min_attack_dist + MINOTAUR_ATTACK_RANGE / 3.0).powi(2))
4468 .then_some(0.1),
4469 );
4470 }
4471 }
4472
4473 pub fn handle_cyclops_attack(
4474 &self,
4475 agent: &mut Agent,
4476 controller: &mut Controller,
4477 attack_data: &AttackData,
4478 tgt_data: &TargetData,
4479 read_data: &ReadData,
4480 ) {
4481 const CYCLOPS_MELEE_RANGE: f32 = 9.0;
4483 const CYCLOPS_FIRE_RANGE: f32 = 30.0;
4485 const CYCLOPS_CHARGE_RANGE: f32 = 18.0;
4487 const SHOCKWAVE_THRESHOLD: f32 = 0.6;
4489
4490 enum FCounters {
4491 ShockwaveThreshold = 0,
4492 }
4493 enum Timers {
4494 AttackChange = 0,
4495 }
4496
4497 if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
4498 agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
4499 }
4500
4501 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4502 if !agent.combat_state.initialized {
4505 agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] =
4506 1.0 - SHOCKWAVE_THRESHOLD;
4507 agent.combat_state.initialized = true;
4508 } else if health_fraction
4509 < agent.combat_state.counters[FCounters::ShockwaveThreshold as usize]
4510 {
4511 controller.push_basic_input(InputKind::Ability(2));
4513
4514 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4515 {
4516 agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] -=
4517 SHOCKWAVE_THRESHOLD;
4518 }
4519 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4520 {
4521 controller.push_basic_input(InputKind::Ability(0));
4523 } else if attack_data.dist_sqrd > CYCLOPS_FIRE_RANGE.powi(2) {
4524 controller.push_basic_input(InputKind::Ability(1));
4526 } else if attack_data.dist_sqrd > CYCLOPS_CHARGE_RANGE.powi(2) {
4527 controller.push_basic_input(InputKind::Secondary);
4529 } else if attack_data.dist_sqrd < CYCLOPS_MELEE_RANGE.powi(2) {
4530 if attack_data.angle < 60.0 {
4531 controller.push_basic_input(InputKind::Primary);
4533 } else if attack_data.angle > 60.0 {
4534 controller.push_basic_input(InputKind::Ability(0));
4536 }
4537 }
4538
4539 self.path_toward_target(
4541 agent,
4542 controller,
4543 tgt_data.pos.0,
4544 read_data,
4545 Path::Partial,
4546 (attack_data.dist_sqrd
4547 < (attack_data.min_attack_dist + CYCLOPS_MELEE_RANGE / 2.0).powi(2))
4548 .then_some(0.1),
4549 );
4550 }
4551
4552 pub fn handle_dullahan_attack(
4553 &self,
4554 agent: &mut Agent,
4555 controller: &mut Controller,
4556 attack_data: &AttackData,
4557 tgt_data: &TargetData,
4558 read_data: &ReadData,
4559 ) {
4560 const MELEE_RANGE: f32 = 9.0;
4562 const LONG_RANGE: f32 = 30.0;
4564 const HP_THRESHOLD: f32 = 0.1;
4566 const MID_RANGE: f32 = 18.0;
4568
4569 enum FCounters {
4570 HealthThreshold = 0,
4571 }
4572 enum Timers {
4573 AttackChange = 0,
4574 }
4575 if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
4576 agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
4577 }
4578
4579 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4580 if !agent.combat_state.initialized {
4583 agent.combat_state.counters[FCounters::HealthThreshold as usize] = 1.0 - HP_THRESHOLD;
4584 agent.combat_state.initialized = true;
4585 } else if health_fraction < agent.combat_state.counters[FCounters::HealthThreshold as usize]
4586 {
4587 controller.push_basic_input(InputKind::Ability(0));
4589
4590 if matches!(
4591 self.char_state.ability_info().map(|ai| ai.input),
4592 Some(InputKind::Ability(0))
4593 ) && matches!(self.char_state.stage_section(), Some(StageSection::Recover))
4594 {
4595 agent.combat_state.counters[FCounters::HealthThreshold as usize] -= HP_THRESHOLD;
4596 }
4597 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4598 {
4599 controller.push_basic_input(InputKind::Ability(0));
4601 } else if attack_data.dist_sqrd > LONG_RANGE.powi(2) {
4602 controller.push_basic_input(InputKind::Ability(1));
4604 } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
4605 controller.push_basic_input(InputKind::Secondary);
4607 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4608 if attack_data.angle < 60.0 {
4609 controller.push_basic_input(InputKind::Primary);
4611 } else if attack_data.angle > 60.0 {
4612 controller.push_basic_input(InputKind::Ability(0));
4614 }
4615 }
4616
4617 self.path_toward_target(
4619 agent,
4620 controller,
4621 tgt_data.pos.0,
4622 read_data,
4623 Path::Full,
4624 (attack_data.dist_sqrd < (attack_data.min_attack_dist + MELEE_RANGE / 2.0).powi(2))
4625 .then_some(0.1),
4626 );
4627 }
4628
4629 pub fn handle_grave_warden_attack(
4630 &self,
4631 agent: &mut Agent,
4632 controller: &mut Controller,
4633 attack_data: &AttackData,
4634 tgt_data: &TargetData,
4635 read_data: &ReadData,
4636 ) {
4637 const GOLEM_MELEE_RANGE: f32 = 4.0;
4638 const GOLEM_LASER_RANGE: f32 = 30.0;
4639 const GOLEM_LONG_RANGE: f32 = 50.0;
4640 const GOLEM_TARGET_SPEED: f32 = 8.0;
4641
4642 enum ActionStateFCounters {
4643 FCounterGlayGolemAttack = 0,
4644 }
4645
4646 let golem_melee_range = self.body.map_or(0.0, |b| b.max_radius()) + GOLEM_MELEE_RANGE;
4647 let health_fraction = self.health.map_or(1.0, |h| h.fraction());
4650 let target_speed_cross_sqd = agent
4652 .target
4653 .as_ref()
4654 .map(|t| t.target)
4655 .and_then(|e| read_data.velocities.get(e))
4656 .map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
4657 let line_of_sight_with_target = || {
4658 entities_have_line_of_sight(
4659 self.pos,
4660 self.body,
4661 self.scale,
4662 tgt_data.pos,
4663 tgt_data.body,
4664 tgt_data.scale,
4665 read_data,
4666 )
4667 };
4668
4669 if attack_data.dist_sqrd < golem_melee_range.powi(2) {
4670 if agent.combat_state.counters[ActionStateFCounters::FCounterGlayGolemAttack as usize]
4671 < 7.5
4672 {
4673 controller.push_basic_input(InputKind::Primary);
4675 agent.combat_state.counters
4676 [ActionStateFCounters::FCounterGlayGolemAttack as usize] += read_data.dt.0;
4677 } else {
4678 controller.push_basic_input(InputKind::Ability(1));
4680 if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
4681 {
4682 agent.combat_state.counters
4683 [ActionStateFCounters::FCounterGlayGolemAttack as usize] = 0.0;
4684 }
4685 }
4686 } else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) {
4687 if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
4688 || target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2)
4689 && line_of_sight_with_target()
4690 && attack_data.angle < 45.0
4691 {
4692 controller.push_basic_input(InputKind::Secondary);
4695 } else if health_fraction < 0.7 {
4696 controller.push_basic_input(InputKind::Ability(0));
4699 }
4700 } else if attack_data.dist_sqrd < GOLEM_LONG_RANGE.powi(2) {
4701 if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() {
4702 controller.push_basic_input(InputKind::Ability(1));
4704 } else if health_fraction < 0.7 {
4705 controller.push_basic_input(InputKind::Ability(0));
4708 }
4709 }
4710
4711 self.path_toward_target(
4713 agent,
4714 controller,
4715 tgt_data.pos.0,
4716 read_data,
4717 Path::Separate,
4718 (attack_data.dist_sqrd
4719 < (attack_data.min_attack_dist + GOLEM_MELEE_RANGE / 1.5).powi(2))
4720 .then_some(0.1),
4721 );
4722 }
4723
4724 pub fn handle_tidal_warrior_attack(
4725 &self,
4726 agent: &mut Agent,
4727 controller: &mut Controller,
4728 attack_data: &AttackData,
4729 tgt_data: &TargetData,
4730 read_data: &ReadData,
4731 ) {
4732 const SCUTTLE_RANGE: f32 = 40.0;
4733 const BUBBLE_RANGE: f32 = 20.0;
4734 const MINION_SUMMON_THRESHOLD: f32 = 0.20;
4735
4736 enum ActionStateConditions {
4737 ConditionCounterInitialized = 0,
4738 }
4739
4740 enum ActionStateFCounters {
4741 FCounterMinionSummonThreshold = 0,
4742 }
4743
4744 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4745 let line_of_sight_with_target = || {
4746 entities_have_line_of_sight(
4747 self.pos,
4748 self.body,
4749 self.scale,
4750 tgt_data.pos,
4751 tgt_data.body,
4752 tgt_data.scale,
4753 read_data,
4754 )
4755 };
4756 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
4757 if !agent.combat_state.conditions
4760 [ActionStateConditions::ConditionCounterInitialized as usize]
4761 {
4762 agent.combat_state.counters
4763 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
4764 1.0 - MINION_SUMMON_THRESHOLD;
4765 agent.combat_state.conditions
4766 [ActionStateConditions::ConditionCounterInitialized as usize] = true;
4767 }
4768
4769 if agent.combat_state.counters[ActionStateFCounters::FCounterMinionSummonThreshold as usize]
4770 > health_fraction
4771 {
4772 controller.push_basic_input(InputKind::Ability(1));
4774
4775 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
4776 {
4777 agent.combat_state.counters
4778 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
4779 MINION_SUMMON_THRESHOLD;
4780 }
4781 } else if attack_data.dist_sqrd < SCUTTLE_RANGE.powi(2) {
4782 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4783 {
4784 controller.push_basic_input(InputKind::Secondary);
4786 } else if attack_data.dist_sqrd < BUBBLE_RANGE.powi(2) {
4787 if matches!(self.char_state, CharacterState::BasicBeam(c) if !matches!(c.stage_section, StageSection::Recover) && c.timer < Duration::from_secs(10))
4788 {
4789 controller.push_basic_input(InputKind::Ability(0));
4792 } else if attack_data.in_min_range() && attack_data.angle < 60.0 {
4793 controller.push_basic_input(InputKind::Primary);
4795 } else if attack_data.angle < 30.0 && line_of_sight_with_target() {
4796 controller.push_basic_input(InputKind::Ability(0));
4799 }
4800 } else if attack_data.angle < 90.0 && line_of_sight_with_target() {
4801 controller.push_basic_input(InputKind::Secondary);
4804 }
4805 }
4806 let path = if tgt_data.pos.0.z < self.pos.0.z {
4807 home
4808 } else {
4809 tgt_data.pos.0
4810 };
4811 self.path_toward_target(agent, controller, path, read_data, Path::Partial, None);
4814 }
4815
4816 pub fn handle_yeti_attack(
4817 &self,
4818 agent: &mut Agent,
4819 controller: &mut Controller,
4820 attack_data: &AttackData,
4821 tgt_data: &TargetData,
4822 read_data: &ReadData,
4823 ) {
4824 const ICE_SPIKES_RANGE: f32 = 15.0;
4825 const ICE_BREATH_RANGE: f32 = 10.0;
4826 const ICE_BREATH_TIMER: f32 = 10.0;
4827 const SNOWBALL_MAX_RANGE: f32 = 50.0;
4828
4829 enum ActionStateFCounters {
4830 FCounterYetiAttack = 0,
4831 }
4832
4833 agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize] +=
4834 read_data.dt.0;
4835
4836 if attack_data.dist_sqrd < ICE_BREATH_RANGE.powi(2) {
4837 if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(2))
4838 {
4839 controller.push_basic_input(InputKind::Ability(0));
4841 } else if agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize]
4842 > ICE_BREATH_TIMER
4843 {
4844 controller.push_basic_input(InputKind::Ability(0));
4846
4847 if matches!(self.char_state, CharacterState::BasicBeam(_)) {
4848 agent.combat_state.counters
4850 [ActionStateFCounters::FCounterYetiAttack as usize] = 0.0;
4851 }
4852 } else if attack_data.in_min_range() {
4853 controller.push_basic_input(InputKind::Primary);
4855 } else {
4856 controller.push_basic_input(InputKind::Secondary);
4858 }
4859 } else if attack_data.dist_sqrd < ICE_SPIKES_RANGE.powi(2) && attack_data.angle < 60.0 {
4860 controller.push_basic_input(InputKind::Secondary);
4862 } else if attack_data.dist_sqrd < SNOWBALL_MAX_RANGE.powi(2) && attack_data.angle < 60.0 {
4863 controller.push_basic_input(InputKind::Ability(1));
4865 }
4866
4867 self.path_toward_target(
4869 agent,
4870 controller,
4871 tgt_data.pos.0,
4872 read_data,
4873 Path::Partial,
4874 attack_data.in_min_range().then_some(0.1),
4875 );
4876 }
4877
4878 pub fn handle_rocksnapper_attack(
4879 &self,
4880 agent: &mut Agent,
4881 controller: &mut Controller,
4882 attack_data: &AttackData,
4883 tgt_data: &TargetData,
4884 read_data: &ReadData,
4885 ) {
4886 const LEAP_TIMER: f32 = 3.0;
4887 const DASH_TIMER: f32 = 5.0;
4888 const LEAP_RANGE: f32 = 20.0;
4889 const MELEE_RANGE: f32 = 5.0;
4890
4891 enum ActionStateTimers {
4892 TimerRocksnapperDash = 0,
4893 TimerRocksnapperLeap = 1,
4894 }
4895 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] +=
4896 read_data.dt.0;
4897 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] +=
4898 read_data.dt.0;
4899
4900 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4901 {
4902 controller.push_basic_input(InputKind::Secondary);
4904 } else if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize]
4905 > DASH_TIMER
4906 {
4907 controller.push_basic_input(InputKind::Secondary);
4909
4910 if matches!(self.char_state, CharacterState::DashMelee(_)) {
4911 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] = 0.0;
4913 }
4914 } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) && attack_data.angle < 90.0 {
4915 if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize]
4916 > LEAP_TIMER
4917 {
4918 controller.push_basic_input(InputKind::Ability(0));
4920
4921 if matches!(self.char_state, CharacterState::LeapShockwave(_)) {
4922 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] =
4924 0.0;
4925 }
4926 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4927 controller.push_basic_input(InputKind::Primary);
4929 }
4930 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
4931 controller.push_basic_input(InputKind::Primary);
4933 }
4934
4935 self.path_toward_target(
4937 agent,
4938 controller,
4939 tgt_data.pos.0,
4940 read_data,
4941 Path::Partial,
4942 None,
4943 );
4944 }
4945
4946 pub fn handle_roshwalr_attack(
4947 &self,
4948 agent: &mut Agent,
4949 controller: &mut Controller,
4950 attack_data: &AttackData,
4951 tgt_data: &TargetData,
4952 read_data: &ReadData,
4953 ) {
4954 const SLOW_CHARGE_RANGE: f32 = 12.5;
4955 const SHOCKWAVE_RANGE: f32 = 12.5;
4956 const SHOCKWAVE_TIMER: f32 = 15.0;
4957 const MELEE_RANGE: f32 = 4.0;
4958
4959 enum ActionStateFCounters {
4960 FCounterRoshwalrAttack = 0,
4961 }
4962
4963 agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] +=
4964 read_data.dt.0;
4965 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4966 {
4967 controller.push_basic_input(InputKind::Ability(0));
4969 } else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 270.0 {
4970 if agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize]
4971 > SHOCKWAVE_TIMER
4972 {
4973 controller.push_basic_input(InputKind::Ability(0));
4975
4976 if matches!(self.char_state, CharacterState::Shockwave(_)) {
4977 agent.combat_state.counters
4979 [ActionStateFCounters::FCounterRoshwalrAttack as usize] = 0.0;
4980 }
4981 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
4982 controller.push_basic_input(InputKind::Primary);
4984 }
4985 } else if attack_data.dist_sqrd > SLOW_CHARGE_RANGE.powi(2) {
4986 controller.push_basic_input(InputKind::Secondary);
4988 }
4989
4990 self.path_toward_target(
4992 agent,
4993 controller,
4994 tgt_data.pos.0,
4995 read_data,
4996 Path::Partial,
4997 None,
4998 );
4999 }
5000
5001 pub fn handle_harvester_attack(
5002 &self,
5003 agent: &mut Agent,
5004 controller: &mut Controller,
5005 attack_data: &AttackData,
5006 tgt_data: &TargetData,
5007 read_data: &ReadData,
5008 rng: &mut impl Rng,
5009 ) {
5010 const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
5024 const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
5025 const PATH_RANGE_FACTOR: f32 = 0.4; const SCYTHE_RANGE_FACTOR: f32 = 0.75; const SCYTHE_AIM_FACTOR: f32 = 0.7;
5028 const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
5029 const FIREBREATH_AIM_FACTOR: f32 = 0.8;
5030 const FIREBREATH_TIME_LIMIT: f32 = 4.0;
5031 const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; const FIREBREATH_COOLDOWN: f32 = 3.5;
5033 const PUMPKIN_RANGE_FACTOR: f32 = 0.75;
5034 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;
5040 const HAS_SUMMONED_SECOND_VINES: usize = 1;
5041 const FIREBREATH: usize = 0;
5043 const MIXUP: usize = 1;
5044 const FAR_PUMPKIN: usize = 2;
5045 const CLOSE_MIXUP_COOLDOWN: usize = 0;
5047 const MID_MIXUP_COOLDOWN: usize = 1;
5048 const FAR_PUMPKIN_COOLDOWN: usize = 2;
5049
5050 let line_of_sight_with_target = || {
5052 entities_have_line_of_sight(
5053 self.pos,
5054 self.body,
5055 self.scale,
5056 tgt_data.pos,
5057 tgt_data.body,
5058 tgt_data.scale,
5059 read_data,
5060 )
5061 };
5062
5063 let (scythe_range, scythe_angle) = {
5066 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
5067 self.extract_ability(AbilityInput::Primary)
5068 {
5069 (range, angle)
5070 } else {
5071 (0.0, 0.0)
5072 }
5073 };
5074 let (firebreath_range, firebreath_angle) = {
5075 if let Some(AbilityData::BasicBeam { range, angle, .. }) =
5076 self.extract_ability(AbilityInput::Secondary)
5077 {
5078 (range, angle)
5079 } else {
5080 (0.0, 0.0)
5081 }
5082 };
5083 let pumpkin_speed = {
5084 if let Some(AbilityData::BasicRanged {
5085 projectile_speed, ..
5086 }) = self.extract_ability(AbilityInput::Auxiliary(0))
5087 {
5088 projectile_speed
5089 } else {
5090 0.0
5091 }
5092 };
5093 let pumpkin_max_range =
5095 projectile_flat_range(pumpkin_speed, self.body.map_or(0.0, |b| b.height()));
5096
5097 let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
5099 let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
5100 let is_in_summon_recovery = matches!(self.char_state, CharacterState::SpriteSummon(data) if matches!(data.stage_section, StageSection::Recover));
5101 let firebreath_timer = if let CharacterState::BasicBeam(data) = self.char_state {
5102 data.timer
5103 } else {
5104 Default::default()
5105 };
5106 let is_using_mixup = is_using_firebreath || is_using_pumpkin;
5107
5108 if !agent.combat_state.initialized {
5110 agent.combat_state.initialized = true;
5111 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5112 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5113 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5114 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5115 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5116 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5117 }
5118
5119 if is_in_summon_recovery {
5123 agent.combat_state.timers[FIREBREATH] = 0.0;
5125 agent.combat_state.timers[MIXUP] = 0.0;
5126 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5127 } else {
5128 if is_using_firebreath {
5130 agent.combat_state.timers[FIREBREATH] = 0.0;
5131 } else {
5132 agent.combat_state.timers[FIREBREATH] += read_data.dt.0;
5133 }
5134 if is_using_mixup {
5135 agent.combat_state.timers[MIXUP] = 0.0;
5136 } else {
5137 agent.combat_state.timers[MIXUP] += read_data.dt.0;
5138 }
5139 if is_using_pumpkin {
5140 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5141 } else {
5142 agent.combat_state.timers[FAR_PUMPKIN] += read_data.dt.0;
5143 }
5144 }
5145
5146 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5148 if health_fraction < SECOND_VINE_CREATION_THRESHOLD
5150 && !agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES]
5151 {
5152 controller.push_basic_input(InputKind::Ability(2));
5154 if is_in_summon_recovery {
5156 agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES] = true;
5157 }
5158 }
5159 else if health_fraction < FIRST_VINE_CREATION_THRESHOLD
5161 && !agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES]
5162 {
5163 controller.push_basic_input(InputKind::Ability(1));
5165 if is_in_summon_recovery {
5167 agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES] = true;
5168 }
5169 }
5170 else if attack_data.dist_sqrd
5172 < (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
5173 {
5174 if is_using_firebreath
5176 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
5177 {
5178 controller.push_basic_input(InputKind::Secondary);
5179 }
5180 if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
5182 if agent.combat_state.timers[MIXUP]
5184 > agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN]
5185 {
5187 if agent.combat_state.timers[FIREBREATH] < FIREBREATH_COOLDOWN {
5189 controller.push_basic_input(InputKind::Ability(0));
5190 }
5191 else if rng.gen_bool(0.5) {
5193 controller.push_basic_input(InputKind::Secondary);
5194 } else {
5195 controller.push_basic_input(InputKind::Ability(0));
5196 }
5197 if is_using_mixup {
5199 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5200 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5201 }
5202 }
5203 else {
5205 controller.push_basic_input(InputKind::Primary);
5206 }
5207 }
5208 } else if attack_data.dist_sqrd < firebreath_range.powi(2) {
5210 #[expect(clippy::if_same_then_else)]
5212 if is_using_firebreath
5213 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
5214 {
5215 controller.push_basic_input(InputKind::Secondary);
5216 }
5217 else if attack_data.dist_sqrd < (firebreath_range * FIREBREATH_RANGE_FACTOR).powi(2)
5219 && attack_data.angle < firebreath_angle * FIREBREATH_AIM_FACTOR
5220 && agent.combat_state.timers[FIREBREATH] > FIREBREATH_COOLDOWN
5221 {
5222 controller.push_basic_input(InputKind::Secondary);
5223 }
5224 else if agent.combat_state.timers[MIXUP]
5226 > agent.combat_state.counters[MID_MIXUP_COOLDOWN]
5227 {
5228 controller.push_basic_input(InputKind::Ability(0));
5229 if is_using_pumpkin {
5231 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5232 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5233 }
5234 }
5235 }
5236 else if attack_data.dist_sqrd < (pumpkin_max_range * PUMPKIN_RANGE_FACTOR).powi(2)
5238 && agent.combat_state.timers[FAR_PUMPKIN]
5239 > agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN]
5240 && line_of_sight_with_target()
5241 {
5242 controller.push_basic_input(InputKind::Ability(0));
5244 if is_using_pumpkin {
5246 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5247 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5248 }
5249 }
5250
5251 if attack_data.dist_sqrd
5254 > (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
5255 {
5256 self.path_toward_target(
5257 agent,
5258 controller,
5259 tgt_data.pos.0,
5260 read_data,
5261 Path::Partial,
5262 None,
5263 );
5264 }
5265 else if attack_data.angle > 0.0 {
5267 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
5269 .xy()
5270 .try_normalized()
5271 .unwrap_or_else(Vec2::zero)
5272 * 0.001; }
5274 }
5275
5276 pub fn handle_frostgigas_attack(
5277 &self,
5278 agent: &mut Agent,
5279 controller: &mut Controller,
5280 attack_data: &AttackData,
5281 tgt_data: &TargetData,
5282 read_data: &ReadData,
5283 rng: &mut impl Rng,
5284 ) {
5285 const GIGAS_MELEE_RANGE: f32 = 12.0;
5286 const GIGAS_SPIKE_RANGE: f32 = 16.0;
5287 const ICEBOMB_RANGE: f32 = 70.0;
5288 const GIGAS_LEAP_RANGE: f32 = 50.0;
5289 const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
5290 const FLASHFREEZE_RANGE: f32 = 30.;
5291
5292 enum ActionStateTimers {
5293 AttackChange,
5294 Bonk,
5295 }
5296
5297 enum ActionStateFCounters {
5298 FCounterMinionSummonThreshold = 0,
5299 }
5300
5301 enum ActionStateICounters {
5302 CurrentAbility = 0,
5307 }
5308
5309 let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0);
5310 let remote_spikes_action = || ControlAction::StartInput {
5311 input: InputKind::Ability(5),
5312 target_entity: None,
5313 select_pos: Some(tgt_data.pos.0),
5314 };
5315
5316 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5317 if !agent.combat_state.initialized {
5320 agent.combat_state.counters
5321 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5322 1.0 - MINION_SUMMON_THRESHOLD;
5323 agent.combat_state.initialized = true;
5324 }
5325
5326 if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 {
5328 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
5329 } else {
5330 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
5331 }
5332 agent.combat_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0;
5333
5334 if health_fraction
5335 < agent.combat_state.counters
5336 [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5337 {
5338 controller.push_basic_input(InputKind::Ability(3));
5340
5341 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5342 {
5343 agent.combat_state.counters
5344 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5345 MINION_SUMMON_THRESHOLD;
5346 }
5347 } else if let Some(ability) = Some(
5349 &mut agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize],
5350 )
5351 .filter(|i| **i != 0)
5352 {
5353 if *ability == 3 && should_use_targeted_spikes() {
5354 *ability = 5
5355 };
5356
5357 let reset = match ability {
5358 1 => {
5360 controller.push_basic_input(InputKind::Ability(1));
5361 matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
5362 },
5363 2 => {
5365 controller.push_basic_input(InputKind::Ability(4));
5366 matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover))
5367 },
5368 3 => {
5370 controller.push_basic_input(InputKind::Ability(0));
5371 matches!(self.char_state, CharacterState::SpriteSummon(c)
5372 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner)))
5373 },
5374 4 => {
5376 controller.push_basic_input(InputKind::Ability(7));
5377 matches!(self.char_state, CharacterState::RapidMelee(c) if matches!(c.stage_section, StageSection::Recover))
5378 },
5379 5 => {
5381 controller.push_action(remote_spikes_action());
5382 matches!(self.char_state, CharacterState::SpriteSummon(c)
5383 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target)))
5384 },
5385 6 => {
5387 controller.push_basic_input(InputKind::Ability(2));
5388 matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5389 },
5390 _ => true,
5392 };
5393
5394 if reset {
5395 *ability = 0;
5396 }
5397 } else if attack_data.dist_sqrd > 5f32.powi(2)
5401 && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6
5403 && rng.gen_bool((0.2 * read_data.dt.0).min(1.0) as f64)
5405 {
5406 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5407 rng.gen_range(5..=6);
5408 } else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
5409 if agent.combat_state.timers[ActionStateTimers::Bonk as usize] > 10. {
5411 controller.push_basic_input(InputKind::Ability(6));
5412
5413 if matches!(self.char_state, CharacterState::BasicMelee(c)
5414 if matches!(c.stage_section, StageSection::Recover) &&
5415 c.static_data.ability_info.ability.is_some_and(|meta| matches!(meta.ability, Ability::MainWeaponAux(6)))
5416 ) {
5417 agent.combat_state.timers[ActionStateTimers::Bonk as usize] =
5418 rng.gen_range(0.0..3.0);
5419 }
5420 } else if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 4.0
5422 && rng.gen_bool(0.1 * read_data.dt.0.min(1.0) as f64)
5423 {
5424 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5425 rng.gen_range(1..=4);
5426 } else if attack_data.angle > 90.0
5429 || agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 5.0
5430 {
5431 if attack_data.angle > 120.0 {
5433 agent.combat_state.int_counters
5434 [ActionStateICounters::CurrentAbility as usize] = 4;
5435 } else {
5436 controller.push_basic_input(InputKind::Secondary);
5437 }
5438 } else {
5439 controller.push_basic_input(InputKind::Primary);
5440 }
5441 } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
5442 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
5443 {
5444 if should_use_targeted_spikes() {
5445 controller.push_action(remote_spikes_action());
5446 } else {
5447 controller.push_basic_input(InputKind::Ability(0));
5448 }
5449 } else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
5450 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
5451 {
5452 controller.push_basic_input(InputKind::Ability(4));
5453 } else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2)
5455 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 3.0
5456 {
5457 controller.push_basic_input(InputKind::Ability(1));
5458 } else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2)
5459 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 3.0
5460 {
5461 controller.push_basic_input(InputKind::Ability(2));
5462 } else {
5464 controller.push_action(remote_spikes_action());
5465 }
5466
5467 self.path_toward_target(
5469 agent,
5470 controller,
5471 tgt_data.pos.0,
5472 read_data,
5473 Path::Partial,
5474 attack_data.in_min_range().then_some(0.1),
5475 );
5476 }
5477
5478 pub fn handle_boreal_hammer_attack(
5479 &self,
5480 agent: &mut Agent,
5481 controller: &mut Controller,
5482 attack_data: &AttackData,
5483 tgt_data: &TargetData,
5484 read_data: &ReadData,
5485 rng: &mut impl Rng,
5486 ) {
5487 enum ActionStateTimers {
5488 TimerHandleHammerAttack = 0,
5489 }
5490
5491 let has_energy = |need| self.energy.current() > need;
5492
5493 let use_leap = |controller: &mut Controller| {
5494 controller.push_basic_input(InputKind::Ability(0));
5495 };
5496
5497 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] +=
5498 read_data.dt.0;
5499
5500 if attack_data.in_min_range() && attack_data.angle < 45.0 {
5501 controller.inputs.move_dir = Vec2::zero();
5502 if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0
5503 {
5504 controller.push_cancel_input(InputKind::Secondary);
5505 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] =
5506 0.0;
5507 } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize]
5508 > 3.0
5509 {
5510 controller.push_basic_input(InputKind::Secondary);
5511 } else if has_energy(50.0) && rng.gen_bool(0.9) {
5512 use_leap(controller);
5513 } else {
5514 controller.push_basic_input(InputKind::Primary);
5515 }
5516 } else {
5517 self.path_toward_target(
5518 agent,
5519 controller,
5520 tgt_data.pos.0,
5521 read_data,
5522 Path::Separate,
5523 None,
5524 );
5525
5526 if attack_data.dist_sqrd < 32.0f32.powi(2)
5527 && entities_have_line_of_sight(
5528 self.pos,
5529 self.body,
5530 self.scale,
5531 tgt_data.pos,
5532 tgt_data.body,
5533 tgt_data.scale,
5534 read_data,
5535 )
5536 {
5537 if rng.gen_bool(0.5) && has_energy(50.0) {
5538 use_leap(controller);
5539 } else if agent.combat_state.timers
5540 [ActionStateTimers::TimerHandleHammerAttack as usize]
5541 > 2.0
5542 {
5543 controller.push_basic_input(InputKind::Secondary);
5544 } else if agent.combat_state.timers
5545 [ActionStateTimers::TimerHandleHammerAttack as usize]
5546 > 4.0
5547 {
5548 controller.push_cancel_input(InputKind::Secondary);
5549 agent.combat_state.timers
5550 [ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0;
5551 }
5552 }
5553 }
5554 }
5555
5556 pub fn handle_boreal_bow_attack(
5557 &self,
5558 agent: &mut Agent,
5559 controller: &mut Controller,
5560 attack_data: &AttackData,
5561 tgt_data: &TargetData,
5562 read_data: &ReadData,
5563 rng: &mut impl Rng,
5564 ) {
5565 let line_of_sight_with_target = || {
5566 entities_have_line_of_sight(
5567 self.pos,
5568 self.body,
5569 self.scale,
5570 tgt_data.pos,
5571 tgt_data.body,
5572 tgt_data.scale,
5573 read_data,
5574 )
5575 };
5576
5577 let has_energy = |need| self.energy.current() > need;
5578
5579 let use_trap = |controller: &mut Controller| {
5580 controller.push_basic_input(InputKind::Ability(0));
5581 };
5582
5583 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5584 if rng.gen_bool(0.5) && has_energy(15.0) {
5585 controller.push_basic_input(InputKind::Secondary);
5586 } else if attack_data.angle < 15.0 {
5587 controller.push_basic_input(InputKind::Primary);
5588 }
5589 } else if attack_data.dist_sqrd < (4.0 * attack_data.min_attack_dist).powi(2)
5590 && line_of_sight_with_target()
5591 {
5592 if rng.gen_bool(0.5) && has_energy(15.0) {
5593 controller.push_basic_input(InputKind::Secondary);
5594 } else if has_energy(20.0) {
5595 use_trap(controller);
5596 }
5597 }
5598
5599 if has_energy(50.0) {
5600 if attack_data.dist_sqrd < (10.0 * attack_data.min_attack_dist).powi(2) {
5601 if let Some((bearing, speed)) = agent.chaser.chase(
5603 &*read_data.terrain,
5604 self.pos.0,
5605 self.vel.0,
5606 tgt_data.pos.0,
5607 TraversalConfig {
5608 min_tgt_dist: 1.25,
5609 ..self.traversal_config
5610 },
5611 ) {
5612 if line_of_sight_with_target() && attack_data.angle < 45.0 {
5613 controller.inputs.move_dir = bearing
5614 .xy()
5615 .rotated_z(rng.gen_range(0.5..1.57))
5616 .try_normalized()
5617 .unwrap_or_else(Vec2::zero)
5618 * 2.0
5619 * speed;
5620 } else {
5621 controller.inputs.move_dir =
5623 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5624 self.jump_if(bearing.z > 1.5, controller);
5625 controller.inputs.move_z = bearing.z;
5626 }
5627 }
5628 } else {
5629 self.path_toward_target(
5631 agent,
5632 controller,
5633 tgt_data.pos.0,
5634 read_data,
5635 Path::Partial,
5636 None,
5637 );
5638 }
5639 } else {
5640 self.path_toward_target(
5642 agent,
5643 controller,
5644 tgt_data.pos.0,
5645 read_data,
5646 Path::Partial,
5647 None,
5648 );
5649 }
5650 }
5651
5652 pub fn handle_cardinal_attack(
5653 &self,
5654 agent: &mut Agent,
5655 controller: &mut Controller,
5656 attack_data: &AttackData,
5657 tgt_data: &TargetData,
5658 read_data: &ReadData,
5659 rng: &mut impl Rng,
5660 ) {
5661 const DESIRED_ENERGY_LEVEL: f32 = 50.0;
5662 const DESIRED_COMBO_LEVEL: u32 = 8;
5663 const MINION_SUMMON_THRESHOLD: f32 = 0.10;
5664
5665 enum ActionStateConditions {
5666 ConditionCounterInitialized = 0,
5667 }
5668
5669 enum ActionStateFCounters {
5670 FCounterHealthThreshold = 0,
5671 }
5672
5673 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5674 if !agent.combat_state.conditions
5677 [ActionStateConditions::ConditionCounterInitialized as usize]
5678 {
5679 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
5680 1.0 - MINION_SUMMON_THRESHOLD;
5681 agent.combat_state.conditions
5682 [ActionStateConditions::ConditionCounterInitialized as usize] = true;
5683 }
5684
5685 if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
5686 > health_fraction
5687 {
5688 controller.push_basic_input(InputKind::Ability(1));
5690
5691 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5692 {
5693 agent.combat_state.counters
5694 [ActionStateFCounters::FCounterHealthThreshold as usize] -=
5695 MINION_SUMMON_THRESHOLD;
5696 }
5697 }
5698 else if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
5700 && entities_have_line_of_sight(
5701 self.pos,
5702 self.body,
5703 self.scale,
5704 tgt_data.pos,
5705 tgt_data.body,
5706 tgt_data.scale,
5707 read_data,
5708 )
5709 {
5710 if self.energy.current() > DESIRED_ENERGY_LEVEL
5713 && read_data
5714 .combos
5715 .get(*self.entity)
5716 .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
5717 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
5718 buff.iter_kind(BuffKind::Regeneration)
5719 .peekable()
5720 .peek()
5721 .is_some()
5722 })
5723 {
5724 controller.push_basic_input(InputKind::Secondary);
5726 } else if self
5727 .skill_set
5728 .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
5729 && self.energy.current() > DESIRED_ENERGY_LEVEL
5730 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
5731 buff.iter_kind(BuffKind::ProtectingWard)
5732 .peekable()
5733 .peek()
5734 .is_some()
5735 })
5736 {
5737 controller.push_basic_input(InputKind::Ability(0));
5740 } else {
5741 controller.push_basic_input(InputKind::Primary);
5744 }
5745 } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5746 if self.body.is_some_and(|b| b.is_humanoid())
5747 && self.energy.current()
5748 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
5749 && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
5750 {
5751 controller.push_basic_input(InputKind::Ability(0));
5753 } else if attack_data.angle < 15.0 {
5754 controller.push_basic_input(InputKind::Primary);
5755 }
5756 }
5757 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5760 if let Some((bearing, speed)) = agent.chaser.chase(
5762 &*read_data.terrain,
5763 self.pos.0,
5764 self.vel.0,
5765 tgt_data.pos.0,
5766 TraversalConfig {
5767 min_tgt_dist: 1.25,
5768 ..self.traversal_config
5769 },
5770 ) {
5771 controller.inputs.move_dir =
5772 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5773 }
5774 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
5775 if let Some((bearing, speed)) = agent.chaser.chase(
5777 &*read_data.terrain,
5778 self.pos.0,
5779 self.vel.0,
5780 tgt_data.pos.0,
5781 TraversalConfig {
5782 min_tgt_dist: 1.25,
5783 ..self.traversal_config
5784 },
5785 ) {
5786 if entities_have_line_of_sight(
5787 self.pos,
5788 self.body,
5789 self.scale,
5790 tgt_data.pos,
5791 tgt_data.body,
5792 tgt_data.scale,
5793 read_data,
5794 ) && attack_data.angle < 45.0
5795 {
5796 controller.inputs.move_dir = bearing
5797 .xy()
5798 .rotated_z(rng.gen_range(0.5..1.57))
5799 .try_normalized()
5800 .unwrap_or_else(Vec2::zero)
5801 * speed;
5802 } else {
5803 controller.inputs.move_dir =
5805 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5806 self.jump_if(bearing.z > 1.5, controller);
5807 controller.inputs.move_z = bearing.z;
5808 }
5809 }
5810 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
5812 && !matches!(self.char_state, CharacterState::BasicAura(_))
5813 && attack_data.dist_sqrd < 16.0f32.powi(2)
5814 && rng.gen::<f32>() < 0.01
5815 {
5816 controller.push_basic_input(InputKind::Roll);
5817 }
5818 } else {
5819 self.path_toward_target(
5821 agent,
5822 controller,
5823 tgt_data.pos.0,
5824 read_data,
5825 Path::Partial,
5826 None,
5827 );
5828 }
5829 }
5830
5831 pub fn handle_sea_bishop_attack(
5832 &self,
5833 agent: &mut Agent,
5834 controller: &mut Controller,
5835 attack_data: &AttackData,
5836 tgt_data: &TargetData,
5837 read_data: &ReadData,
5838 rng: &mut impl Rng,
5839 ) {
5840 let line_of_sight_with_target = || {
5841 entities_have_line_of_sight(
5842 self.pos,
5843 self.body,
5844 self.scale,
5845 tgt_data.pos,
5846 tgt_data.body,
5847 tgt_data.scale,
5848 read_data,
5849 )
5850 };
5851
5852 enum ActionStateTimers {
5853 TimerBeam = 0,
5854 }
5855 if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 6.0 {
5856 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
5857 } else {
5858 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
5859 }
5860
5861 if line_of_sight_with_target()
5863 && agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 3.0
5864 {
5865 controller.push_basic_input(InputKind::Primary);
5866 }
5867 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5870 if let Some((bearing, speed)) = agent.chaser.chase(
5872 &*read_data.terrain,
5873 self.pos.0,
5874 self.vel.0,
5875 tgt_data.pos.0,
5876 TraversalConfig {
5877 min_tgt_dist: 1.25,
5878 ..self.traversal_config
5879 },
5880 ) {
5881 controller.inputs.move_dir =
5882 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5883 }
5884 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
5885 if let Some((bearing, speed)) = agent.chaser.chase(
5887 &*read_data.terrain,
5888 self.pos.0,
5889 self.vel.0,
5890 tgt_data.pos.0,
5891 TraversalConfig {
5892 min_tgt_dist: 1.25,
5893 ..self.traversal_config
5894 },
5895 ) {
5896 if line_of_sight_with_target() && attack_data.angle < 45.0 {
5897 controller.inputs.move_dir = bearing
5898 .xy()
5899 .rotated_z(rng.gen_range(0.5..1.57))
5900 .try_normalized()
5901 .unwrap_or_else(Vec2::zero)
5902 * speed;
5903 } else {
5904 controller.inputs.move_dir =
5906 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5907 self.jump_if(bearing.z > 1.5, controller);
5908 controller.inputs.move_z = bearing.z;
5909 }
5910 }
5911 } else {
5912 self.path_toward_target(
5914 agent,
5915 controller,
5916 tgt_data.pos.0,
5917 read_data,
5918 Path::Partial,
5919 None,
5920 );
5921 }
5922 }
5923
5924 pub fn handle_cursekeeper_attack(
5925 &self,
5926 agent: &mut Agent,
5927 controller: &mut Controller,
5928 attack_data: &AttackData,
5929 tgt_data: &TargetData,
5930 read_data: &ReadData,
5931 rng: &mut impl Rng,
5932 ) {
5933 enum ActionStateTimers {
5934 TimerBeam,
5935 TimerSummon,
5936 SelectSummon,
5937 }
5938 if tgt_data.pos.0.z - self.pos.0.z > 3.5 {
5939 controller.push_action(ControlAction::StartInput {
5940 input: InputKind::Ability(4),
5941 target_entity: agent
5942 .target
5943 .as_ref()
5944 .and_then(|t| read_data.uids.get(t.target))
5945 .copied(),
5946 select_pos: None,
5947 });
5948 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 12.0 {
5949 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
5950 } else {
5951 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
5952 }
5953
5954 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
5955 {
5956 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] = 0.0;
5957 agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] =
5958 rng.gen_range(0..=3) as f32;
5959 } else {
5960 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] += read_data.dt.0;
5961 }
5962
5963 if agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] > 32.0 {
5964 match agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] as i32 {
5965 0 => controller.push_basic_input(InputKind::Ability(0)),
5966 1 => controller.push_basic_input(InputKind::Ability(1)),
5967 2 => controller.push_basic_input(InputKind::Ability(2)),
5968 _ => controller.push_basic_input(InputKind::Ability(3)),
5969 }
5970 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 6.0 {
5971 controller.push_basic_input(InputKind::Ability(5));
5972 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 9.0 {
5973 controller.push_basic_input(InputKind::Primary);
5974 } else {
5975 controller.push_basic_input(InputKind::Secondary);
5976 }
5977
5978 if attack_data.dist_sqrd > 10_f32.powi(2)
5979 || agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 4.0
5980 {
5981 self.path_toward_target(
5982 agent,
5983 controller,
5984 tgt_data.pos.0,
5985 read_data,
5986 Path::Full,
5987 None,
5988 );
5989 }
5990 }
5991
5992 pub fn handle_shamanic_spirit_attack(
5993 &self,
5994 agent: &mut Agent,
5995 controller: &mut Controller,
5996 attack_data: &AttackData,
5997 tgt_data: &TargetData,
5998 read_data: &ReadData,
5999 ) {
6000 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
6001 controller.push_action(ControlAction::StartInput {
6002 input: InputKind::Secondary,
6003 target_entity: agent
6004 .target
6005 .as_ref()
6006 .and_then(|t| read_data.uids.get(t.target))
6007 .copied(),
6008 select_pos: None,
6009 });
6010 } else if attack_data.in_min_range() && attack_data.angle < 30.0 {
6011 controller.push_basic_input(InputKind::Primary);
6012 controller.inputs.move_dir = Vec2::zero();
6013 } else {
6014 self.path_toward_target(
6015 agent,
6016 controller,
6017 tgt_data.pos.0,
6018 read_data,
6019 Path::Full,
6020 None,
6021 );
6022 }
6023 }
6024
6025 pub fn handle_cursekeeper_fake_attack(
6026 &self,
6027 controller: &mut Controller,
6028 attack_data: &AttackData,
6029 ) {
6030 if attack_data.dist_sqrd < 25_f32.powi(2) {
6031 controller.push_basic_input(InputKind::Primary);
6032 }
6033 }
6034
6035 pub fn handle_karkatha_attack(
6036 &self,
6037 agent: &mut Agent,
6038 controller: &mut Controller,
6039 attack_data: &AttackData,
6040 tgt_data: &TargetData,
6041 read_data: &ReadData,
6042 _rng: &mut impl Rng,
6043 ) {
6044 enum ActionStateTimers {
6045 RiposteTimer,
6046 SummonTimer,
6047 }
6048
6049 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] += read_data.dt.0;
6050 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] += read_data.dt.0;
6051 if matches!(self.char_state, CharacterState::RiposteMelee(c) if !matches!(c.stage_section, StageSection::Recover))
6052 {
6053 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] = 0.0;
6055 }
6056 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
6057 {
6058 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] = 0.0;
6060 }
6061 let home = agent.patrol_origin.unwrap_or(self.pos.0);
6063 let dest = if tgt_data.pos.0.z < self.pos.0.z {
6064 home
6065 } else {
6066 tgt_data.pos.0
6067 };
6068 if attack_data.in_min_range() {
6069 if agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] > 3.0 {
6070 controller.push_basic_input(InputKind::Ability(2));
6071 } else {
6072 controller.push_basic_input(InputKind::Primary);
6073 };
6074 } else if attack_data.dist_sqrd < 20.0_f32.powi(2) {
6075 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] > 20.0 {
6076 controller.push_basic_input(InputKind::Ability(1));
6077 } else {
6078 controller.push_basic_input(InputKind::Secondary);
6079 }
6080 } else if attack_data.dist_sqrd < 30.0_f32.powi(2) {
6081 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] < 10.0 {
6082 self.path_toward_target(
6083 agent,
6084 controller,
6085 tgt_data.pos.0,
6086 read_data,
6087 Path::Full,
6088 None,
6089 );
6090 } else {
6091 controller.push_basic_input(InputKind::Ability(0));
6092 }
6093 } else {
6094 self.path_toward_target(agent, controller, dest, read_data, Path::Full, None);
6095 }
6096 }
6097
6098 pub fn handle_dagon_attack(
6099 &self,
6100 agent: &mut Agent,
6101 controller: &mut Controller,
6102 attack_data: &AttackData,
6103 tgt_data: &TargetData,
6104 read_data: &ReadData,
6105 ) {
6106 enum ActionStateTimers {
6107 TimerDagon = 0,
6108 }
6109 let line_of_sight_with_target = || {
6110 entities_have_line_of_sight(
6111 self.pos,
6112 self.body,
6113 self.scale,
6114 tgt_data.pos,
6115 tgt_data.body,
6116 tgt_data.scale,
6117 read_data,
6118 )
6119 };
6120 let home = agent.patrol_origin.unwrap_or(self.pos.0);
6122 let exit = Vec3::new(home.x - 6.0, home.y - 6.0, home.z);
6123 let (station_0, station_1) = (exit + 12.0, exit - 12.0);
6124 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.5 {
6125 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] = 0.0;
6126 }
6127 if !line_of_sight_with_target()
6128 && (tgt_data.pos.0 - exit).xy().magnitude_squared() < (10.0_f32).powi(2)
6129 {
6130 let station = if (tgt_data.pos.0 - station_0).xy().magnitude_squared()
6131 < (tgt_data.pos.0 - station_1).xy().magnitude_squared()
6132 {
6133 station_0
6134 } else {
6135 station_1
6136 };
6137 self.path_toward_target(agent, controller, station, read_data, Path::Full, None);
6138 }
6139 else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6141 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6142 controller.push_basic_input(InputKind::Primary);
6143 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6144 } else {
6145 controller.push_basic_input(InputKind::Secondary);
6146 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6147 }
6148 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6150 controller.inputs.move_dir = Vec2::zero();
6151 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6152 controller.push_basic_input(InputKind::Primary);
6153 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6154 } else {
6155 controller.push_basic_input(InputKind::Ability(1));
6156 }
6157 } else if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) {
6158 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6160 controller.push_basic_input(InputKind::Primary);
6161 } else {
6162 controller.push_basic_input(InputKind::Ability(2));
6163 }
6164 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6165 } else if line_of_sight_with_target() {
6166 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6168 controller.push_basic_input(InputKind::Primary);
6169 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6170 } else {
6171 controller.push_basic_input(InputKind::Ability(0));
6172 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6173 }
6174 }
6175 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6177 Path::Separate
6178 } else {
6179 Path::Partial
6180 };
6181 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
6182 }
6183
6184 pub fn handle_snaretongue_attack(
6185 &self,
6186 agent: &mut Agent,
6187 controller: &mut Controller,
6188 attack_data: &AttackData,
6189 read_data: &ReadData,
6190 ) {
6191 enum Timers {
6192 TimerAttack = 0,
6193 }
6194 let attack_timer = &mut agent.combat_state.timers[Timers::TimerAttack as usize];
6195 if *attack_timer > 2.5 {
6196 *attack_timer = 0.0;
6197 }
6198 if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
6200 if *attack_timer > 0.5 {
6201 controller.push_basic_input(InputKind::Primary);
6202 *attack_timer += read_data.dt.0;
6203 } else {
6204 controller.push_basic_input(InputKind::Secondary);
6205 *attack_timer += read_data.dt.0;
6206 }
6207 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6209 controller.inputs.move_dir = Vec2::zero();
6210 if *attack_timer > 2.0 {
6211 controller.push_basic_input(InputKind::Ability(0));
6212 *attack_timer += read_data.dt.0;
6213 } else {
6214 controller.push_basic_input(InputKind::Ability(1));
6215 }
6216 } else {
6217 if *attack_timer > 1.0 {
6219 controller.push_basic_input(InputKind::Ability(0));
6220 *attack_timer += read_data.dt.0;
6221 } else {
6222 controller.push_basic_input(InputKind::Ability(2));
6223 *attack_timer += read_data.dt.0;
6224 }
6225 }
6226 }
6227
6228 pub fn handle_deadwood(
6229 &self,
6230 agent: &mut Agent,
6231 controller: &mut Controller,
6232 attack_data: &AttackData,
6233 tgt_data: &TargetData,
6234 read_data: &ReadData,
6235 ) {
6236 const BEAM_RANGE: f32 = 20.0;
6237 const BEAM_TIME: Duration = Duration::from_secs(3);
6238 if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
6240 {
6241 controller.push_basic_input(InputKind::Secondary);
6243 controller.inputs.move_dir = self.ori.look_vec().xy();
6244 } else if attack_data.in_min_range() && attack_data.angle_xy < 10.0 {
6245 controller.push_basic_input(InputKind::Secondary);
6247 } else if matches!(self.char_state, CharacterState::BasicBeam(s) if s.stage_section != StageSection::Recover && s.timer < BEAM_TIME)
6248 {
6249 controller.push_basic_input(InputKind::Primary);
6251 } else if attack_data.dist_sqrd < BEAM_RANGE.powi(2) {
6252 if attack_data.angle_xy < 5.0 {
6254 controller.push_basic_input(InputKind::Primary);
6255 } else {
6256 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6258 .xy()
6259 .try_normalized()
6260 .unwrap_or_else(Vec2::zero)
6261 * 0.01;
6262 }
6263 } else {
6264 self.path_toward_target(
6266 agent,
6267 controller,
6268 tgt_data.pos.0,
6269 read_data,
6270 Path::Partial,
6271 None,
6272 );
6273 }
6274 }
6275
6276 pub fn handle_mandragora(
6277 &self,
6278 agent: &mut Agent,
6279 controller: &mut Controller,
6280 attack_data: &AttackData,
6281 tgt_data: &TargetData,
6282 read_data: &ReadData,
6283 ) {
6284 const SCREAM_RANGE: f32 = 10.0; enum ActionStateFCounters {
6287 FCounterHealthThreshold = 0,
6288 }
6289
6290 enum ActionStateConditions {
6291 ConditionHasScreamed = 0,
6292 }
6293
6294 if !agent.combat_state.initialized {
6295 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6296 self.health.map_or(0.0, |h| h.maximum());
6297 agent.combat_state.initialized = true;
6298 }
6299
6300 if !agent.combat_state.conditions[ActionStateConditions::ConditionHasScreamed as usize] {
6301 if self.health.is_some_and(|h| {
6304 h.current()
6305 < agent.combat_state.counters
6306 [ActionStateFCounters::FCounterHealthThreshold as usize]
6307 }) || attack_data.dist_sqrd < SCREAM_RANGE.powi(2)
6308 {
6309 agent.combat_state.conditions
6310 [ActionStateConditions::ConditionHasScreamed as usize] = true;
6311 controller.push_basic_input(InputKind::Secondary);
6312 }
6313 } else {
6314 if attack_data.in_min_range() {
6316 controller.push_basic_input(InputKind::Primary);
6317 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
6318 && entities_have_line_of_sight(
6319 self.pos,
6320 self.body,
6321 self.scale,
6322 tgt_data.pos,
6323 tgt_data.body,
6324 tgt_data.scale,
6325 read_data,
6326 )
6327 {
6328 self.path_toward_target(
6330 agent,
6331 controller,
6332 tgt_data.pos.0,
6333 read_data,
6334 Path::Partial,
6335 None,
6336 );
6337 } else {
6338 agent.combat_state.conditions
6340 [ActionStateConditions::ConditionHasScreamed as usize] = false;
6341 agent.combat_state.counters
6342 [ActionStateFCounters::FCounterHealthThreshold as usize] =
6343 self.health.map_or(0.0, |h| h.maximum());
6344 }
6345 }
6346 }
6347
6348 pub fn handle_wood_golem(
6349 &self,
6350 agent: &mut Agent,
6351 controller: &mut Controller,
6352 attack_data: &AttackData,
6353 tgt_data: &TargetData,
6354 read_data: &ReadData,
6355 rng: &mut impl Rng,
6356 ) {
6357 const PATH_RANGE_FACTOR: f32 = 0.3; const STRIKE_RANGE_FACTOR: f32 = 0.6; const STRIKE_AIM_FACTOR: f32 = 0.7;
6372 const SPIN_RANGE_FACTOR: f32 = 0.6;
6373 const SPIN_COOLDOWN: f32 = 1.5;
6374 const SPIN_RELAX_FACTOR: f32 = 0.2;
6375 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.7;
6376 const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
6377 const SHOCKWAVE_COOLDOWN: f32 = 5.0;
6378 const MIXUP_COOLDOWN: f32 = 2.5;
6379 const MIXUP_RELAX_FACTOR: f32 = 0.3;
6380
6381 const SPIN: usize = 0;
6383 const SHOCKWAVE: usize = 1;
6384 const MIXUP: usize = 2;
6385
6386 let shockwave_min_range = self.body.map_or(0.0, |b| b.height() * 1.1);
6389
6390 let (strike_range, strike_angle) = {
6392 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
6393 self.extract_ability(AbilityInput::Primary)
6394 {
6395 (range, angle)
6396 } else {
6397 (0.0, 0.0)
6398 }
6399 };
6400 let spin_range = {
6401 if let Some(AbilityData::BasicMelee { range, .. }) =
6402 self.extract_ability(AbilityInput::Secondary)
6403 {
6404 range
6405 } else {
6406 0.0
6407 }
6408 };
6409 let (shockwave_max_range, shockwave_angle) = {
6410 if let Some(AbilityData::Shockwave { range, angle, .. }) =
6411 self.extract_ability(AbilityInput::Auxiliary(0))
6412 {
6413 (range, angle)
6414 } else {
6415 (0.0, 0.0)
6416 }
6417 };
6418
6419 let is_in_spin_range = attack_data.dist_sqrd
6421 < (attack_data.body_dist + spin_range * SPIN_RANGE_FACTOR).powi(2);
6422 let is_in_strike_range = attack_data.dist_sqrd
6423 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
6424 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
6425
6426 let current_input = self.char_state.ability_info().map(|ai| ai.input);
6431 if matches!(current_input, Some(InputKind::Secondary)) {
6432 agent.combat_state.timers[SPIN] = 0.0;
6434 agent.combat_state.timers[MIXUP] = 0.0;
6435 } else if is_in_spin_range && !(is_in_strike_range && is_in_strike_angle) {
6436 agent.combat_state.timers[SPIN] += read_data.dt.0;
6438 } else {
6439 agent.combat_state.timers[SPIN] =
6441 (agent.combat_state.timers[SPIN] - read_data.dt.0 * SPIN_RELAX_FACTOR).max(0.0);
6442 }
6443 if matches!(self.char_state, CharacterState::Shockwave(_)) {
6445 agent.combat_state.timers[SHOCKWAVE] = 0.0;
6447 agent.combat_state.timers[MIXUP] = 0.0;
6448 } else {
6449 agent.combat_state.timers[SHOCKWAVE] += read_data.dt.0;
6451 }
6452 if is_in_strike_range && is_in_strike_angle {
6454 agent.combat_state.timers[MIXUP] += read_data.dt.0;
6456 } else {
6457 agent.combat_state.timers[MIXUP] =
6459 (agent.combat_state.timers[MIXUP] - read_data.dt.0 * MIXUP_RELAX_FACTOR).max(0.0);
6460 }
6461
6462 if is_in_strike_range && is_in_strike_angle {
6465 if agent.combat_state.timers[MIXUP] > MIXUP_COOLDOWN {
6467 let randomise: u8 = rng.gen_range(1..=3);
6468 match randomise {
6469 1 => controller.push_basic_input(InputKind::Ability(0)), 2 => controller.push_basic_input(InputKind::Primary), _ => controller.push_basic_input(InputKind::Secondary), }
6473 }
6474 else {
6476 controller.push_basic_input(InputKind::Primary);
6477 }
6478 }
6479 else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
6481 if agent.combat_state.timers[SPIN] > SPIN_COOLDOWN {
6483 controller.push_basic_input(InputKind::Secondary);
6484 }
6485 }
6487 else if attack_data.dist_sqrd > shockwave_min_range.powi(2)
6489 && attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
6490 && attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
6491 {
6492 if agent.combat_state.timers[SHOCKWAVE] > SHOCKWAVE_COOLDOWN {
6494 controller.push_basic_input(InputKind::Ability(0));
6495 }
6496 }
6498
6499 if attack_data.dist_sqrd
6502 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
6503 {
6504 self.path_toward_target(
6505 agent,
6506 controller,
6507 tgt_data.pos.0,
6508 read_data,
6509 Path::Partial,
6510 None,
6511 );
6512 }
6513 else if attack_data.angle > 0.0 {
6515 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6517 .xy()
6518 .try_normalized()
6519 .unwrap_or_else(Vec2::zero)
6520 * 0.001; }
6522 }
6523
6524 pub fn handle_gnarling_chieftain(
6525 &self,
6526 agent: &mut Agent,
6527 controller: &mut Controller,
6528 attack_data: &AttackData,
6529 tgt_data: &TargetData,
6530 read_data: &ReadData,
6531 rng: &mut impl Rng,
6532 ) {
6533 const PATH_RANGE_FACTOR: f32 = 0.4;
6548 const STRIKE_RANGE_FACTOR: f32 = 0.7;
6549 const STRIKE_AIM_FACTOR: f32 = 0.8;
6550 const BARRAGE_RANGE_FACTOR: f32 = 0.8;
6551 const BARRAGE_AIM_FACTOR: f32 = 0.65;
6552 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
6553 const TOTEM_COOLDOWN: f32 = 25.0;
6554 const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
6555 const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
6556 const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
6557
6558 const HAS_SUMMONED_FIRST_TOTEM: usize = 0;
6560 const SUMMON_TOTEM: usize = 0;
6562 const HEAVY_ATTACK: usize = 1;
6563 const HEAVY_ATTACK_COOLDOWN: usize = 0;
6565
6566 let line_of_sight_with_target = || {
6568 entities_have_line_of_sight(
6569 self.pos,
6570 self.body,
6571 self.scale,
6572 tgt_data.pos,
6573 tgt_data.body,
6574 tgt_data.scale,
6575 read_data,
6576 )
6577 };
6578
6579 let (strike_range, strike_angle) = {
6582 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
6583 self.extract_ability(AbilityInput::Primary)
6584 {
6585 (range, angle)
6586 } else {
6587 (0.0, 0.0)
6588 }
6589 };
6590 let (barrage_speed, barrage_spread, barrage_count) = {
6591 if let Some(AbilityData::BasicRanged {
6592 projectile_speed,
6593 projectile_spread,
6594 num_projectiles,
6595 ..
6596 }) = self.extract_ability(AbilityInput::Secondary)
6597 {
6598 (
6599 projectile_speed,
6600 projectile_spread,
6601 num_projectiles.compute(self.heads.map_or(1, |heads| heads.amount() as u32)),
6602 )
6603 } else {
6604 (0.0, 0.0, 0)
6605 }
6606 };
6607 let shockwave_range = {
6608 if let Some(AbilityData::Shockwave { range, .. }) =
6609 self.extract_ability(AbilityInput::Auxiliary(0))
6610 {
6611 range
6612 } else {
6613 0.0
6614 }
6615 };
6616
6617 let barrage_max_range =
6619 projectile_flat_range(barrage_speed, self.body.map_or(2.0, |b| b.height()));
6620 let barrange_angle = projectile_multi_angle(barrage_spread, barrage_count);
6621
6622 let is_in_strike_range = attack_data.dist_sqrd
6624 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
6625 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
6626
6627 if !agent.combat_state.initialized {
6629 agent.combat_state.initialized = true;
6630 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
6631 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
6632 }
6633
6634 match self.char_state {
6639 CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
6640 agent.combat_state.timers[SUMMON_TOTEM] = 0.0;
6642 agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] = true;
6643 },
6644 CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
6645 agent.combat_state.counters[HEAVY_ATTACK] = 0.0;
6647 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
6648 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
6649 },
6650 _ => {},
6651 }
6652 agent.combat_state.timers[SUMMON_TOTEM] += read_data.dt.0;
6654 if is_in_strike_range {
6656 if is_in_strike_angle {
6658 agent.combat_state.counters[HEAVY_ATTACK] += read_data.dt.0;
6659 } else {
6660 agent.combat_state.counters[HEAVY_ATTACK] +=
6662 read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
6663 }
6664 } else {
6665 agent.combat_state.counters[HEAVY_ATTACK] +=
6667 read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
6668 }
6669
6670 if !agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] {
6673 controller.push_basic_input(InputKind::Ability(2));
6674 }
6675 else if agent.combat_state.timers[SUMMON_TOTEM] > TOTEM_COOLDOWN {
6677 controller.push_basic_input(InputKind::Ability(rng.gen_range(1..=3)));
6678 }
6679 else if agent.combat_state.counters[HEAVY_ATTACK]
6683 > agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN]
6684 && attack_data.dist_sqrd < (barrage_max_range * BARRAGE_RANGE_FACTOR).powi(2)
6685 {
6686 if line_of_sight_with_target() {
6688 if attack_data.angle > barrange_angle * BARRAGE_AIM_FACTOR {
6690 controller.push_basic_input(InputKind::Ability(0));
6691 }
6692 else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
6694 {
6695 if rng.gen_bool(0.5) {
6696 controller.push_basic_input(InputKind::Secondary);
6697 } else {
6698 controller.push_basic_input(InputKind::Ability(0));
6699 }
6700 }
6701 else {
6703 controller.push_basic_input(InputKind::Secondary);
6704 }
6705 }
6707 else {
6709 if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
6711 controller.push_basic_input(InputKind::Ability(0));
6712 }
6713 }
6715 }
6716 else if is_in_strike_range && is_in_strike_angle {
6718 controller.push_basic_input(InputKind::Primary);
6719 }
6720 if attack_data.dist_sqrd
6725 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
6726 {
6727 self.path_toward_target(
6728 agent,
6729 controller,
6730 tgt_data.pos.0,
6731 read_data,
6732 Path::Full,
6733 None,
6734 );
6735 }
6736 else if attack_data.angle > 0.0 {
6738 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6740 .xy()
6741 .try_normalized()
6742 .unwrap_or_else(Vec2::zero)
6743 * 0.001; }
6745 }
6746
6747 pub fn handle_sword_simple_attack(
6748 &self,
6749 agent: &mut Agent,
6750 controller: &mut Controller,
6751 attack_data: &AttackData,
6752 tgt_data: &TargetData,
6753 read_data: &ReadData,
6754 ) {
6755 const DASH_TIMER: usize = 0;
6756 agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
6757 if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
6758 {
6759 controller.push_basic_input(InputKind::Secondary);
6760 } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
6761 if agent.combat_state.timers[DASH_TIMER] > 2.0 {
6762 agent.combat_state.timers[DASH_TIMER] = 0.0;
6763 }
6764 controller.push_basic_input(InputKind::Primary);
6765 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
6766 && self
6767 .path_toward_target(
6768 agent,
6769 controller,
6770 tgt_data.pos.0,
6771 read_data,
6772 Path::Separate,
6773 None,
6774 )
6775 .is_some()
6776 && entities_have_line_of_sight(
6777 self.pos,
6778 self.body,
6779 self.scale,
6780 tgt_data.pos,
6781 tgt_data.body,
6782 tgt_data.scale,
6783 read_data,
6784 )
6785 && agent.combat_state.timers[DASH_TIMER] > 4.0
6786 && attack_data.angle < 45.0
6787 {
6788 controller.push_basic_input(InputKind::Secondary);
6789 agent.combat_state.timers[DASH_TIMER] = 0.0;
6790 } else {
6791 self.path_toward_target(
6792 agent,
6793 controller,
6794 tgt_data.pos.0,
6795 read_data,
6796 Path::Partial,
6797 None,
6798 );
6799 }
6800 }
6801
6802 pub fn handle_adlet_hunter(
6803 &self,
6804 agent: &mut Agent,
6805 controller: &mut Controller,
6806 attack_data: &AttackData,
6807 tgt_data: &TargetData,
6808 read_data: &ReadData,
6809 rng: &mut impl Rng,
6810 ) {
6811 const ROTATE_TIMER: usize = 0;
6812 const ROTATE_DIR_CONDITION: usize = 0;
6813 agent.combat_state.timers[ROTATE_TIMER] -= read_data.dt.0;
6814 if agent.combat_state.timers[ROTATE_TIMER] < 0.0 {
6815 agent.combat_state.conditions[ROTATE_DIR_CONDITION] = rng.gen_bool(0.5);
6816 agent.combat_state.timers[ROTATE_TIMER] = rng.gen::<f32>() * 5.0;
6817 }
6818 let primary = self.extract_ability(AbilityInput::Primary);
6819 let secondary = self.extract_ability(AbilityInput::Secondary);
6820 let could_use_input = |input| match input {
6821 InputKind::Primary => primary.as_ref().is_some_and(|p| {
6822 p.could_use(
6823 attack_data,
6824 self,
6825 tgt_data,
6826 read_data,
6827 AbilityPreferences::default(),
6828 )
6829 }),
6830 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
6831 s.could_use(
6832 attack_data,
6833 self,
6834 tgt_data,
6835 read_data,
6836 AbilityPreferences::default(),
6837 )
6838 }),
6839 _ => false,
6840 };
6841 let move_forwards = if could_use_input(InputKind::Primary) {
6842 controller.push_basic_input(InputKind::Primary);
6843 false
6844 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 8_f32.powi(2) {
6845 controller.push_basic_input(InputKind::Secondary);
6846 true
6847 } else {
6848 true
6849 };
6850
6851 if move_forwards && attack_data.dist_sqrd > 3_f32.powi(2) {
6852 self.path_toward_target(
6853 agent,
6854 controller,
6855 tgt_data.pos.0,
6856 read_data,
6857 Path::Separate,
6858 None,
6859 );
6860 } else {
6861 self.path_toward_target(
6862 agent,
6863 controller,
6864 tgt_data.pos.0,
6865 read_data,
6866 Path::Separate,
6867 None,
6868 );
6869 let dir = if agent.combat_state.conditions[ROTATE_DIR_CONDITION] {
6870 1.0
6871 } else {
6872 -1.0
6873 };
6874 controller.inputs.move_dir.rotate_z(PI / 2.0 * dir);
6875 }
6876 }
6877
6878 pub fn handle_adlet_icepicker(
6879 &self,
6880 agent: &mut Agent,
6881 controller: &mut Controller,
6882 attack_data: &AttackData,
6883 tgt_data: &TargetData,
6884 read_data: &ReadData,
6885 ) {
6886 let primary = self.extract_ability(AbilityInput::Primary);
6887 let secondary = self.extract_ability(AbilityInput::Secondary);
6888 let could_use_input = |input| match input {
6889 InputKind::Primary => primary.as_ref().is_some_and(|p| {
6890 p.could_use(
6891 attack_data,
6892 self,
6893 tgt_data,
6894 read_data,
6895 AbilityPreferences::default(),
6896 )
6897 }),
6898 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
6899 s.could_use(
6900 attack_data,
6901 self,
6902 tgt_data,
6903 read_data,
6904 AbilityPreferences::default(),
6905 )
6906 }),
6907 _ => false,
6908 };
6909 let move_forwards = if could_use_input(InputKind::Primary) {
6910 controller.push_basic_input(InputKind::Primary);
6911 false
6912 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 5_f32.powi(2) {
6913 controller.push_basic_input(InputKind::Secondary);
6914 false
6915 } else {
6916 true
6917 };
6918
6919 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
6920 self.path_toward_target(
6921 agent,
6922 controller,
6923 tgt_data.pos.0,
6924 read_data,
6925 Path::Separate,
6926 None,
6927 );
6928 }
6929 }
6930
6931 pub fn handle_adlet_tracker(
6932 &self,
6933 agent: &mut Agent,
6934 controller: &mut Controller,
6935 attack_data: &AttackData,
6936 tgt_data: &TargetData,
6937 read_data: &ReadData,
6938 ) {
6939 const TRAP_TIMER: usize = 0;
6940 agent.combat_state.timers[TRAP_TIMER] += read_data.dt.0;
6941 if agent.combat_state.timers[TRAP_TIMER] > 20.0 {
6942 agent.combat_state.timers[TRAP_TIMER] = 0.0;
6943 }
6944 let primary = self.extract_ability(AbilityInput::Primary);
6945 let could_use_input = |input| match input {
6946 InputKind::Primary => primary.as_ref().is_some_and(|p| {
6947 p.could_use(
6948 attack_data,
6949 self,
6950 tgt_data,
6951 read_data,
6952 AbilityPreferences::default(),
6953 )
6954 }),
6955 _ => false,
6956 };
6957 let move_forwards = if agent.combat_state.timers[TRAP_TIMER] < 3.0 {
6958 controller.push_basic_input(InputKind::Secondary);
6959 false
6960 } else if could_use_input(InputKind::Primary) {
6961 controller.push_basic_input(InputKind::Primary);
6962 false
6963 } else {
6964 true
6965 };
6966
6967 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
6968 self.path_toward_target(
6969 agent,
6970 controller,
6971 tgt_data.pos.0,
6972 read_data,
6973 Path::Separate,
6974 None,
6975 );
6976 }
6977 }
6978
6979 pub fn handle_adlet_elder(
6980 &self,
6981 agent: &mut Agent,
6982 controller: &mut Controller,
6983 attack_data: &AttackData,
6984 tgt_data: &TargetData,
6985 read_data: &ReadData,
6986 rng: &mut impl Rng,
6987 ) {
6988 const TRAP_TIMER: usize = 0;
6989 agent.combat_state.timers[TRAP_TIMER] -= read_data.dt.0;
6990 if matches!(self.char_state, CharacterState::BasicRanged(_)) {
6991 agent.combat_state.timers[TRAP_TIMER] = 15.0;
6992 }
6993 let primary = self.extract_ability(AbilityInput::Primary);
6994 let secondary = self.extract_ability(AbilityInput::Secondary);
6995 let abilities = [
6996 self.extract_ability(AbilityInput::Auxiliary(0)),
6997 self.extract_ability(AbilityInput::Auxiliary(1)),
6998 ];
6999 let could_use_input = |input| match input {
7000 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7001 p.could_use(
7002 attack_data,
7003 self,
7004 tgt_data,
7005 read_data,
7006 AbilityPreferences::default(),
7007 )
7008 }),
7009 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7010 s.could_use(
7011 attack_data,
7012 self,
7013 tgt_data,
7014 read_data,
7015 AbilityPreferences::default(),
7016 )
7017 }),
7018 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7019 a.could_use(
7020 attack_data,
7021 self,
7022 tgt_data,
7023 read_data,
7024 AbilityPreferences::default(),
7025 )
7026 }),
7027 _ => false,
7028 };
7029 let move_forwards = if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
7030 {
7031 controller.push_basic_input(InputKind::Secondary);
7032 false
7033 } else if agent.combat_state.timers[TRAP_TIMER] < 0.0 && !tgt_data.considered_ranged() {
7034 controller.push_basic_input(InputKind::Ability(0));
7035 false
7036 } else if could_use_input(InputKind::Primary) {
7037 controller.push_basic_input(InputKind::Primary);
7038 false
7039 } else if could_use_input(InputKind::Secondary) && rng.gen_bool(0.5) {
7040 controller.push_basic_input(InputKind::Secondary);
7041 false
7042 } else if could_use_input(InputKind::Ability(1)) {
7043 controller.push_basic_input(InputKind::Ability(1));
7044 false
7045 } else {
7046 true
7047 };
7048
7049 if matches!(self.char_state, CharacterState::LeapMelee(_)) {
7050 let tgt_vec = tgt_data.pos.0.xy() - self.pos.0.xy();
7051 if tgt_vec.magnitude_squared() > 2_f32.powi(2) {
7052 if let Some(look_dir) = Dir::from_unnormalized(Vec3::from(tgt_vec)) {
7053 controller.inputs.look_dir = look_dir;
7054 }
7055 }
7056 }
7057
7058 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7059 self.path_toward_target(
7060 agent,
7061 controller,
7062 tgt_data.pos.0,
7063 read_data,
7064 Path::Separate,
7065 None,
7066 );
7067 }
7068 }
7069
7070 pub fn handle_icedrake(
7071 &self,
7072 agent: &mut Agent,
7073 controller: &mut Controller,
7074 attack_data: &AttackData,
7075 tgt_data: &TargetData,
7076 read_data: &ReadData,
7077 rng: &mut impl Rng,
7078 ) {
7079 let primary = self.extract_ability(AbilityInput::Primary);
7080 let secondary = self.extract_ability(AbilityInput::Secondary);
7081 let abilities = [
7082 self.extract_ability(AbilityInput::Auxiliary(0)),
7083 self.extract_ability(AbilityInput::Auxiliary(1)),
7084 ];
7085 let could_use_input = |input| match input {
7086 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7087 p.could_use(
7088 attack_data,
7089 self,
7090 tgt_data,
7091 read_data,
7092 AbilityPreferences::default(),
7093 )
7094 }),
7095 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7096 s.could_use(
7097 attack_data,
7098 self,
7099 tgt_data,
7100 read_data,
7101 AbilityPreferences::default(),
7102 )
7103 }),
7104 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7105 a.could_use(
7106 attack_data,
7107 self,
7108 tgt_data,
7109 read_data,
7110 AbilityPreferences::default(),
7111 )
7112 }),
7113 _ => false,
7114 };
7115
7116 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7117 Some(input @ InputKind::Primary) => {
7118 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7119 && could_use_input(input)
7120 {
7121 controller.push_basic_input(input);
7122 true
7123 } else {
7124 false
7125 }
7126 },
7127 Some(input @ InputKind::Ability(1)) => {
7128 if self
7129 .char_state
7130 .timer()
7131 .is_some_and(|t| t.as_secs_f32() < 3.0)
7132 && could_use_input(input)
7133 {
7134 controller.push_basic_input(input);
7135 true
7136 } else {
7137 false
7138 }
7139 },
7140 _ => false,
7141 };
7142
7143 let move_forwards = if !continued_attack {
7144 if could_use_input(InputKind::Primary) && rng.gen_bool(0.4) {
7145 controller.push_basic_input(InputKind::Primary);
7146 false
7147 } else if could_use_input(InputKind::Secondary) && rng.gen_bool(0.8) {
7148 controller.push_basic_input(InputKind::Secondary);
7149 false
7150 } else if could_use_input(InputKind::Ability(1)) && rng.gen_bool(0.9) {
7151 controller.push_basic_input(InputKind::Ability(1));
7152 true
7153 } else if could_use_input(InputKind::Ability(0)) {
7154 controller.push_basic_input(InputKind::Ability(0));
7155 true
7156 } else {
7157 true
7158 }
7159 } else {
7160 false
7161 };
7162
7163 if move_forwards {
7164 self.path_toward_target(
7165 agent,
7166 controller,
7167 tgt_data.pos.0,
7168 read_data,
7169 Path::Separate,
7170 None,
7171 );
7172 }
7173 }
7174
7175 pub fn handle_hydra(
7176 &self,
7177 agent: &mut Agent,
7178 controller: &mut Controller,
7179 attack_data: &AttackData,
7180 tgt_data: &TargetData,
7181 read_data: &ReadData,
7182 rng: &mut impl Rng,
7183 ) {
7184 enum ActionStateTimers {
7185 RegrowHeadNoDamage,
7186 RegrowHeadNoAttack,
7187 }
7188
7189 let could_use_input = |input| {
7190 Option::from(input)
7191 .and_then(|ability| {
7192 Some(self.extract_ability(ability)?.could_use(
7193 attack_data,
7194 self,
7195 tgt_data,
7196 read_data,
7197 AbilityPreferences::default(),
7198 ))
7199 })
7200 .unwrap_or(false)
7201 };
7202
7203 const FOCUS_ATTACK_RANGE: f32 = 5.0;
7204
7205 if attack_data.dist_sqrd < FOCUS_ATTACK_RANGE.powi(2) {
7206 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] = 0.0;
7207 } else {
7208 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] +=
7209 read_data.dt.0;
7210 }
7211
7212 if let Some(health) = self.health.filter(|health| health.last_change.amount < 0.0) {
7213 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] =
7214 (read_data.time.0 - health.last_change.time.0) as f32;
7215 } else {
7216 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] +=
7217 read_data.dt.0;
7218 }
7219
7220 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
7221 match self.char_state {
7222 CharacterState::ChargedMelee(c) => {
7223 if c.charge_frac() < 1.0 && could_use_input(input) {
7224 controller.push_basic_input(input);
7225 }
7226 },
7227 CharacterState::ChargedRanged(c) => {
7228 if c.charge_frac() < 1.0 && could_use_input(input) {
7229 controller.push_basic_input(input);
7230 }
7231 },
7232 _ => {},
7233 }
7234 }
7235
7236 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7237 Some(input @ InputKind::Primary) => {
7238 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7239 && could_use_input(input)
7240 {
7241 controller.push_basic_input(input);
7242 true
7243 } else {
7244 false
7245 }
7246 },
7247 _ => false,
7248 };
7249
7250 let has_heads = self.heads.is_none_or(|heads| heads.amount() > 0);
7251
7252 let move_forwards = if !continued_attack {
7253 if could_use_input(InputKind::Ability(1))
7254 && rng.gen_bool(0.9)
7255 && (agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] > 5.0
7256 || agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize]
7257 > 6.0)
7258 && self.heads.is_some_and(|heads| heads.amount_missing() > 0)
7259 {
7260 controller.push_basic_input(InputKind::Ability(2));
7261 false
7262 } else if has_heads && could_use_input(InputKind::Primary) && rng.gen_bool(0.8) {
7263 controller.push_basic_input(InputKind::Primary);
7264 true
7265 } else if has_heads && could_use_input(InputKind::Secondary) && rng.gen_bool(0.4) {
7266 controller.push_basic_input(InputKind::Secondary);
7267 false
7268 } else if has_heads && could_use_input(InputKind::Ability(1)) && rng.gen_bool(0.6) {
7269 controller.push_basic_input(InputKind::Ability(1));
7270 true
7271 } else if !has_heads && could_use_input(InputKind::Ability(3)) && rng.gen_bool(0.7) {
7272 controller.push_basic_input(InputKind::Ability(3));
7273 true
7274 } else if could_use_input(InputKind::Ability(0)) {
7275 controller.push_basic_input(InputKind::Ability(0));
7276 true
7277 } else {
7278 true
7279 }
7280 } else {
7281 true
7282 };
7283
7284 if move_forwards {
7285 if has_heads {
7286 self.path_toward_target(
7287 agent,
7288 controller,
7289 tgt_data.pos.0,
7290 read_data,
7291 Path::Separate,
7292 (attack_data.dist_sqrd
7294 < (2.5 + self.body.map_or(0.0, |b| b.front_radius())).powi(2))
7295 .then_some(0.3),
7296 );
7297 } else {
7298 self.flee(agent, controller, read_data, tgt_data.pos);
7299 }
7300 }
7301 }
7302
7303 pub fn handle_random_abilities(
7304 &self,
7305 agent: &mut Agent,
7306 controller: &mut Controller,
7307 attack_data: &AttackData,
7308 tgt_data: &TargetData,
7309 read_data: &ReadData,
7310 rng: &mut impl Rng,
7311 primary_weight: u8,
7312 secondary_weight: u8,
7313 ability_weights: [u8; BASE_ABILITY_LIMIT],
7314 ) {
7315 let primary = self.extract_ability(AbilityInput::Primary);
7316 let secondary = self.extract_ability(AbilityInput::Secondary);
7317 let abilities = [
7318 self.extract_ability(AbilityInput::Auxiliary(0)),
7319 self.extract_ability(AbilityInput::Auxiliary(1)),
7320 self.extract_ability(AbilityInput::Auxiliary(2)),
7321 self.extract_ability(AbilityInput::Auxiliary(3)),
7322 self.extract_ability(AbilityInput::Auxiliary(4)),
7323 ];
7324 let could_use_input = |input| match input {
7325 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7326 p.could_use(
7327 attack_data,
7328 self,
7329 tgt_data,
7330 read_data,
7331 AbilityPreferences::default(),
7332 )
7333 }),
7334 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7335 s.could_use(
7336 attack_data,
7337 self,
7338 tgt_data,
7339 read_data,
7340 AbilityPreferences::default(),
7341 )
7342 }),
7343 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7344 a.could_use(
7345 attack_data,
7346 self,
7347 tgt_data,
7348 read_data,
7349 AbilityPreferences::default(),
7350 )
7351 }),
7352 _ => false,
7353 };
7354
7355 let primary_chance = primary_weight as f64
7356 / ((primary_weight + secondary_weight + ability_weights.iter().sum::<u8>()) as f64)
7357 .max(0.01);
7358 let secondary_chance = secondary_weight as f64
7359 / ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
7360 let ability_chances = {
7361 let mut chances = [0.0; BASE_ABILITY_LIMIT];
7362 chances.iter_mut().enumerate().for_each(|(i, chance)| {
7363 *chance = ability_weights[i] as f64
7364 / (ability_weights
7365 .iter()
7366 .enumerate()
7367 .filter_map(|(j, weight)| if j >= i { Some(weight) } else { None })
7368 .sum::<u8>() as f64)
7369 .max(0.01)
7370 });
7371 chances
7372 };
7373
7374 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
7375 match self.char_state {
7376 CharacterState::ChargedMelee(c) => {
7377 if c.charge_frac() < 1.0 && could_use_input(input) {
7378 controller.push_basic_input(input);
7379 }
7380 },
7381 CharacterState::ChargedRanged(c) => {
7382 if c.charge_frac() < 1.0 && could_use_input(input) {
7383 controller.push_basic_input(input);
7384 }
7385 },
7386 _ => {},
7387 }
7388 }
7389
7390 let move_forwards = if could_use_input(InputKind::Primary) && rng.gen_bool(primary_chance) {
7391 controller.push_basic_input(InputKind::Primary);
7392 false
7393 } else if could_use_input(InputKind::Secondary) && rng.gen_bool(secondary_chance) {
7394 controller.push_basic_input(InputKind::Secondary);
7395 false
7396 } else if could_use_input(InputKind::Ability(0)) && rng.gen_bool(ability_chances[0]) {
7397 controller.push_basic_input(InputKind::Ability(0));
7398 false
7399 } else if could_use_input(InputKind::Ability(1)) && rng.gen_bool(ability_chances[1]) {
7400 controller.push_basic_input(InputKind::Ability(1));
7401 false
7402 } else if could_use_input(InputKind::Ability(2)) && rng.gen_bool(ability_chances[2]) {
7403 controller.push_basic_input(InputKind::Ability(2));
7404 false
7405 } else if could_use_input(InputKind::Ability(3)) && rng.gen_bool(ability_chances[3]) {
7406 controller.push_basic_input(InputKind::Ability(3));
7407 false
7408 } else if could_use_input(InputKind::Ability(4)) && rng.gen_bool(ability_chances[4]) {
7409 controller.push_basic_input(InputKind::Ability(4));
7410 false
7411 } else {
7412 true
7413 };
7414
7415 if move_forwards {
7416 self.path_toward_target(
7417 agent,
7418 controller,
7419 tgt_data.pos.0,
7420 read_data,
7421 Path::Separate,
7422 None,
7423 );
7424 }
7425 }
7426
7427 pub fn handle_simple_double_attack(
7428 &self,
7429 agent: &mut Agent,
7430 controller: &mut Controller,
7431 attack_data: &AttackData,
7432 tgt_data: &TargetData,
7433 read_data: &ReadData,
7434 ) {
7435 const MAX_ATTACK_RANGE: f32 = 20.0;
7436
7437 if attack_data.angle < 60.0 && attack_data.dist_sqrd < MAX_ATTACK_RANGE.powi(2) {
7438 controller.inputs.move_dir = Vec2::zero();
7439 if attack_data.in_min_range() {
7440 controller.push_basic_input(InputKind::Primary);
7441 } else {
7442 controller.push_basic_input(InputKind::Secondary);
7443 }
7444 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
7445 self.path_toward_target(
7446 agent,
7447 controller,
7448 tgt_data.pos.0,
7449 read_data,
7450 Path::Separate,
7451 None,
7452 );
7453 } else {
7454 self.path_toward_target(
7455 agent,
7456 controller,
7457 tgt_data.pos.0,
7458 read_data,
7459 Path::Partial,
7460 None,
7461 );
7462 }
7463 }
7464
7465 pub fn handle_clay_steed_attack(
7466 &self,
7467 agent: &mut Agent,
7468 controller: &mut Controller,
7469 attack_data: &AttackData,
7470 tgt_data: &TargetData,
7471 read_data: &ReadData,
7472 ) {
7473 enum ActionStateTimers {
7474 AttackTimer,
7475 }
7476 const HOOF_ATTACK_RANGE: f32 = 1.0;
7477 const HOOF_ATTACK_ANGLE: f32 = 50.0;
7478
7479 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
7480 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 10.0 {
7481 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
7483 }
7484
7485 if attack_data.angle < HOOF_ATTACK_ANGLE
7486 && attack_data.dist_sqrd
7487 < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.max_radius())).powi(2)
7488 {
7489 controller.inputs.move_dir = Vec2::zero();
7490 controller.push_basic_input(InputKind::Primary);
7491 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
7492 controller.push_basic_input(InputKind::Secondary);
7493 } else {
7494 self.path_toward_target(
7495 agent,
7496 controller,
7497 tgt_data.pos.0,
7498 read_data,
7499 Path::Full,
7500 None,
7501 );
7502 }
7503 }
7504
7505 pub fn handle_ancient_effigy_attack(
7506 &self,
7507 agent: &mut Agent,
7508 controller: &mut Controller,
7509 attack_data: &AttackData,
7510 tgt_data: &TargetData,
7511 read_data: &ReadData,
7512 ) {
7513 enum ActionStateTimers {
7514 BlastTimer,
7515 }
7516
7517 let home = agent.patrol_origin.unwrap_or(self.pos.0);
7518 let line_of_sight_with_target = || {
7519 entities_have_line_of_sight(
7520 self.pos,
7521 self.body,
7522 self.scale,
7523 tgt_data.pos,
7524 tgt_data.body,
7525 tgt_data.scale,
7526 read_data,
7527 )
7528 };
7529 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] += read_data.dt.0;
7530
7531 if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] > 6.0 {
7532 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] = 0.0;
7533 }
7534 if line_of_sight_with_target() {
7535 if attack_data.in_min_range() {
7536 controller.push_basic_input(InputKind::Secondary);
7537 } else if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] < 2.0 {
7538 controller.push_basic_input(InputKind::Primary);
7539 } else {
7540 self.path_toward_target(
7541 agent,
7542 controller,
7543 tgt_data.pos.0,
7544 read_data,
7545 Path::Separate,
7546 None,
7547 );
7548 }
7549 } else {
7550 if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
7552 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
7553 }
7554 }
7555 }
7556
7557 pub fn handle_clay_golem_attack(
7558 &self,
7559 agent: &mut Agent,
7560 controller: &mut Controller,
7561 attack_data: &AttackData,
7562 tgt_data: &TargetData,
7563 read_data: &ReadData,
7564 ) {
7565 const MIN_DASH_RANGE: f32 = 15.0;
7566
7567 enum ActionStateTimers {
7568 AttackTimer,
7569 }
7570
7571 let line_of_sight_with_target = || {
7572 entities_have_line_of_sight(
7573 self.pos,
7574 self.body,
7575 self.scale,
7576 tgt_data.pos,
7577 tgt_data.body,
7578 tgt_data.scale,
7579 read_data,
7580 )
7581 };
7582 let spawn = agent.patrol_origin.unwrap_or(self.pos.0);
7583 let home = Vec3::new(spawn.x - 32.0, spawn.y - 12.0, spawn.z);
7584 let is_home = (home - self.pos.0).xy().magnitude_squared() < (3.0_f32).powi(2);
7585 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
7586 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
7587 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
7589 }
7590 if line_of_sight_with_target() {
7591 controller.inputs.move_dir = Vec2::zero();
7592 if attack_data.in_min_range() {
7593 controller.push_basic_input(InputKind::Primary);
7594 } else if attack_data.dist_sqrd > MIN_DASH_RANGE.powi(2) {
7595 controller.push_basic_input(InputKind::Secondary);
7596 } else {
7597 self.path_toward_target(
7598 agent,
7599 controller,
7600 tgt_data.pos.0,
7601 read_data,
7602 Path::Partial,
7603 None,
7604 );
7605 }
7606 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.0 {
7607 if !is_home {
7608 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
7610 } else {
7611 self.path_toward_target(agent, controller, spawn, read_data, Path::Separate, None);
7612 }
7613 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
7614 self.path_toward_target(
7615 agent,
7616 controller,
7617 tgt_data.pos.0,
7618 read_data,
7619 Path::Separate,
7620 None,
7621 );
7622 }
7623 }
7624
7625 pub fn handle_haniwa_soldier(
7626 &self,
7627 agent: &mut Agent,
7628 controller: &mut Controller,
7629 attack_data: &AttackData,
7630 tgt_data: &TargetData,
7631 read_data: &ReadData,
7632 ) {
7633 const DEFENSIVE_CONDITION: usize = 0;
7634 const RIPOSTE_TIMER: usize = 0;
7635 const MODE_CYCLE_TIMER: usize = 1;
7636
7637 let primary = self.extract_ability(AbilityInput::Primary);
7638 let secondary = self.extract_ability(AbilityInput::Secondary);
7639 let could_use_input = |input| match input {
7640 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7641 p.could_use(
7642 attack_data,
7643 self,
7644 tgt_data,
7645 read_data,
7646 AbilityPreferences::default(),
7647 )
7648 }),
7649 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7650 s.could_use(
7651 attack_data,
7652 self,
7653 tgt_data,
7654 read_data,
7655 AbilityPreferences::default(),
7656 )
7657 }),
7658 _ => false,
7659 };
7660
7661 agent.combat_state.timers[RIPOSTE_TIMER] += read_data.dt.0;
7662 agent.combat_state.timers[MODE_CYCLE_TIMER] += read_data.dt.0;
7663
7664 if agent.combat_state.timers[MODE_CYCLE_TIMER] > 7.0 {
7665 agent.combat_state.conditions[DEFENSIVE_CONDITION] =
7666 !agent.combat_state.conditions[DEFENSIVE_CONDITION];
7667 agent.combat_state.timers[MODE_CYCLE_TIMER] = 0.0;
7668 }
7669
7670 if matches!(self.char_state, CharacterState::RiposteMelee(_)) {
7671 agent.combat_state.timers[RIPOSTE_TIMER] = 0.0;
7672 }
7673
7674 let try_move = if agent.combat_state.conditions[DEFENSIVE_CONDITION] {
7675 controller.push_basic_input(InputKind::Block);
7676 true
7677 } else if agent.combat_state.timers[RIPOSTE_TIMER] > 10.0
7678 && could_use_input(InputKind::Secondary)
7679 {
7680 controller.push_basic_input(InputKind::Secondary);
7681 false
7682 } else if could_use_input(InputKind::Primary) {
7683 controller.push_basic_input(InputKind::Primary);
7684 false
7685 } else {
7686 true
7687 };
7688
7689 if try_move && attack_data.dist_sqrd > 2_f32.powi(2) {
7690 self.path_toward_target(
7691 agent,
7692 controller,
7693 tgt_data.pos.0,
7694 read_data,
7695 Path::Separate,
7696 None,
7697 );
7698 }
7699 }
7700
7701 pub fn handle_haniwa_guard(
7702 &self,
7703 agent: &mut Agent,
7704 controller: &mut Controller,
7705 attack_data: &AttackData,
7706 tgt_data: &TargetData,
7707 read_data: &ReadData,
7708 rng: &mut impl Rng,
7709 ) {
7710 const BACKPEDAL_DIST: f32 = 5.0;
7711 const ROTATE_CCW_CONDITION: usize = 0;
7712 const FLURRY_TIMER: usize = 0;
7713 const BACKPEDAL_TIMER: usize = 1;
7714 const SWITCH_ROTATE_TIMER: usize = 2;
7715 const SWITCH_ROTATE_COUNTER: usize = 0;
7716
7717 let primary = self.extract_ability(AbilityInput::Primary);
7718 let secondary = self.extract_ability(AbilityInput::Secondary);
7719 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
7720 let could_use_input = |input| match input {
7721 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7722 p.could_use(
7723 attack_data,
7724 self,
7725 tgt_data,
7726 read_data,
7727 AbilityPreferences::default(),
7728 )
7729 }),
7730 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7731 s.could_use(
7732 attack_data,
7733 self,
7734 tgt_data,
7735 read_data,
7736 AbilityPreferences::default(),
7737 )
7738 }),
7739 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7740 a.could_use(
7741 attack_data,
7742 self,
7743 tgt_data,
7744 read_data,
7745 AbilityPreferences::default(),
7746 )
7747 }),
7748 _ => false,
7749 };
7750
7751 if !agent.combat_state.initialized {
7752 agent.combat_state.conditions[ROTATE_CCW_CONDITION] = rng.gen_bool(0.5);
7753 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.gen_range(5.0..20.0);
7754 agent.combat_state.initialized = true;
7755 }
7756
7757 let continue_flurry = match self.char_state {
7758 CharacterState::BasicMelee(_) => {
7759 agent.combat_state.timers[FLURRY_TIMER] += read_data.dt.0;
7760 false
7761 },
7762 CharacterState::RapidMelee(c) => {
7763 agent.combat_state.timers[FLURRY_TIMER] = 0.0;
7764 !matches!(c.stage_section, StageSection::Recover)
7765 },
7766 CharacterState::ComboMelee2(_) => {
7767 agent.combat_state.timers[BACKPEDAL_TIMER] = 0.0;
7768 false
7769 },
7770 _ => false,
7771 };
7772 agent.combat_state.timers[SWITCH_ROTATE_TIMER] += read_data.dt.0;
7773 agent.combat_state.timers[BACKPEDAL_TIMER] += read_data.dt.0;
7774
7775 if agent.combat_state.timers[SWITCH_ROTATE_TIMER]
7776 > agent.combat_state.counters[SWITCH_ROTATE_COUNTER]
7777 {
7778 agent.combat_state.conditions[ROTATE_CCW_CONDITION] =
7779 !agent.combat_state.conditions[ROTATE_CCW_CONDITION];
7780 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.gen_range(5.0..20.0);
7781 }
7782
7783 let move_farther = attack_data.dist_sqrd < BACKPEDAL_DIST.powi(2);
7784 let move_closer = if continue_flurry && could_use_input(InputKind::Secondary) {
7785 controller.push_basic_input(InputKind::Secondary);
7786 false
7787 } else if agent.combat_state.timers[BACKPEDAL_TIMER] > 10.0
7788 && move_farther
7789 && could_use_input(InputKind::Ability(0))
7790 {
7791 controller.push_basic_input(InputKind::Ability(0));
7792 false
7793 } else if agent.combat_state.timers[FLURRY_TIMER] > 6.0
7794 && could_use_input(InputKind::Secondary)
7795 {
7796 controller.push_basic_input(InputKind::Secondary);
7797 false
7798 } else if could_use_input(InputKind::Primary) {
7799 controller.push_basic_input(InputKind::Primary);
7800 false
7801 } else {
7802 true
7803 };
7804
7805 if let Some((bearing, speed)) = agent.chaser.chase(
7806 &*read_data.terrain,
7807 self.pos.0,
7808 self.vel.0,
7809 tgt_data.pos.0,
7810 TraversalConfig {
7811 min_tgt_dist: 1.25,
7812 ..self.traversal_config
7813 },
7814 ) {
7815 if entities_have_line_of_sight(
7816 self.pos,
7817 self.body,
7818 self.scale,
7819 tgt_data.pos,
7820 tgt_data.body,
7821 tgt_data.scale,
7822 read_data,
7823 ) && attack_data.angle < 45.0
7824 {
7825 let angle = match (
7826 agent.combat_state.conditions[ROTATE_CCW_CONDITION],
7827 move_closer,
7828 move_farther,
7829 ) {
7830 (true, true, false) => rng.gen_range(-1.5..-0.5),
7831 (true, false, true) => rng.gen_range(-2.2..-1.7),
7832 (true, _, _) => rng.gen_range(-1.7..-1.5),
7833 (false, true, false) => rng.gen_range(0.5..1.5),
7834 (false, false, true) => rng.gen_range(1.7..2.2),
7835 (false, _, _) => rng.gen_range(1.5..1.7),
7836 };
7837 controller.inputs.move_dir = bearing
7838 .xy()
7839 .rotated_z(angle)
7840 .try_normalized()
7841 .unwrap_or_else(Vec2::zero)
7842 * speed;
7843 } else {
7844 controller.inputs.move_dir =
7845 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
7846 self.jump_if(bearing.z > 1.5, controller);
7847 }
7848 }
7849 }
7850
7851 pub fn handle_haniwa_archer(
7852 &self,
7853 agent: &mut Agent,
7854 controller: &mut Controller,
7855 attack_data: &AttackData,
7856 tgt_data: &TargetData,
7857 read_data: &ReadData,
7858 ) {
7859 const KICK_TIMER: usize = 0;
7860 const EXPLOSIVE_TIMER: usize = 1;
7861
7862 let primary = self.extract_ability(AbilityInput::Primary);
7863 let secondary = self.extract_ability(AbilityInput::Secondary);
7864 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
7865 let could_use_input = |input| match input {
7866 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7867 p.could_use(
7868 attack_data,
7869 self,
7870 tgt_data,
7871 read_data,
7872 AbilityPreferences::default(),
7873 )
7874 }),
7875 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7876 s.could_use(
7877 attack_data,
7878 self,
7879 tgt_data,
7880 read_data,
7881 AbilityPreferences::default(),
7882 )
7883 }),
7884 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7885 a.could_use(
7886 attack_data,
7887 self,
7888 tgt_data,
7889 read_data,
7890 AbilityPreferences::default(),
7891 )
7892 }),
7893 _ => false,
7894 };
7895
7896 agent.combat_state.timers[KICK_TIMER] += read_data.dt.0;
7897 agent.combat_state.timers[EXPLOSIVE_TIMER] += read_data.dt.0;
7898
7899 match self.char_state.ability_info().map(|ai| ai.input) {
7900 Some(InputKind::Secondary) => {
7901 agent.combat_state.timers[KICK_TIMER] = 0.0;
7902 },
7903 Some(InputKind::Ability(0)) => {
7904 agent.combat_state.timers[EXPLOSIVE_TIMER] = 0.0;
7905 },
7906 _ => {},
7907 }
7908
7909 if agent.combat_state.timers[KICK_TIMER] > 4.0 && could_use_input(InputKind::Secondary) {
7910 controller.push_basic_input(InputKind::Secondary);
7911 } else if agent.combat_state.timers[EXPLOSIVE_TIMER] > 15.0
7912 && could_use_input(InputKind::Ability(0))
7913 {
7914 controller.push_basic_input(InputKind::Ability(0));
7915 } else if could_use_input(InputKind::Primary) {
7916 controller.push_basic_input(InputKind::Primary);
7917 } else {
7918 self.path_toward_target(
7919 agent,
7920 controller,
7921 tgt_data.pos.0,
7922 read_data,
7923 Path::Separate,
7924 None,
7925 );
7926 }
7927 }
7928
7929 pub fn handle_terracotta_statue_attack(
7930 &self,
7931 agent: &mut Agent,
7932 controller: &mut Controller,
7933 attack_data: &AttackData,
7934 read_data: &ReadData,
7935 ) {
7936 enum Conditions {
7937 AttackToggle,
7938 }
7939 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
7940 if (home - self.pos.0).xy().magnitude_squared() > (2.0_f32).powi(2) {
7942 self.path_toward_target(agent, controller, home, read_data, Path::Full, None);
7943 } else if !agent.combat_state.conditions[Conditions::AttackToggle as usize] {
7944 controller.push_basic_input(InputKind::Primary);
7946 } else {
7947 controller.inputs.move_dir = Vec2::zero();
7948 if attack_data.dist_sqrd < 8.5f32.powi(2) {
7949 controller.push_basic_input(InputKind::Primary);
7951 } else {
7952 controller.push_basic_input(InputKind::Secondary);
7954 }
7955 }
7956 if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
7957 {
7958 agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
7959 }
7960 }
7961
7962 pub fn handle_jiangshi_attack(
7963 &self,
7964 agent: &mut Agent,
7965 controller: &mut Controller,
7966 attack_data: &AttackData,
7967 tgt_data: &TargetData,
7968 read_data: &ReadData,
7969 ) {
7970 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
7971 controller.push_action(ControlAction::StartInput {
7972 input: InputKind::Secondary,
7973 target_entity: agent
7974 .target
7975 .as_ref()
7976 .and_then(|t| read_data.uids.get(t.target))
7977 .copied(),
7978 select_pos: None,
7979 });
7980 } else if attack_data.dist_sqrd < 12.0f32.powi(2) {
7981 controller.push_basic_input(InputKind::Primary);
7982 }
7983
7984 self.path_toward_target(
7985 agent,
7986 controller,
7987 tgt_data.pos.0,
7988 read_data,
7989 Path::Full,
7990 None,
7991 );
7992 }
7993}