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::{
12 AbilityReqItem, ActiveAbilities, AuxiliaryAbility, BASE_ABILITY_LIMIT, BowStance,
13 Stance, SwordStance,
14 },
15 buff::BuffKind,
16 fluid_dynamics::LiquidKind,
17 skills::{AxeSkill, BowSkill, HammerSkill, SceptreSkill, Skill, StaffSkill, SwordSkill},
18 },
19 consts::GRAVITY,
20 path::TraversalConfig,
21 states::{
22 self_buff,
23 sprite_summon::{self, SpriteSummonAnchor},
24 utils::StageSection,
25 },
26 terrain::Block,
27 util::Dir,
28 vol::ReadVol,
29};
30use rand::{RngExt, seq::IndexedRandom};
31use std::{f32::consts::PI, time::Duration};
32use vek::*;
33use world::util::CARDINALS;
34
35fn projectile_flat_range(speed: f32, height: f32) -> f32 {
37 let w = speed.powi(2);
38 let u = 0.5 * 2_f32.sqrt() * speed;
39 (0.5 * w + u * (0.5 * w + 2.0 * GRAVITY * height).sqrt()) / GRAVITY
40}
41
42fn projectile_multi_angle(projectile_spread: f32, num_projectiles: u32) -> f32 {
44 (180.0 / PI) * projectile_spread * (num_projectiles - 1) as f32
45}
46
47fn rng_from_span(rng: &mut impl RngExt, span: [f32; 2]) -> f32 {
48 rng.random_range(span[0]..=span[1])
49}
50
51impl AgentData<'_> {
52 pub fn handle_simple_melee(
55 &self,
56 agent: &mut Agent,
57 controller: &mut Controller,
58 attack_data: &AttackData,
59 tgt_data: &TargetData,
60 read_data: &ReadData,
61 rng: &mut impl RngExt,
62 ) {
63 if attack_data.in_min_range() && attack_data.angle < 30.0 {
64 controller.push_basic_input(InputKind::Primary);
65 controller.inputs.move_dir = Vec2::zero();
66 } else {
67 self.path_toward_target(
68 agent,
69 controller,
70 tgt_data.pos.0,
71 read_data,
72 Path::AtTarget,
73 None,
74 );
75 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
76 && attack_data.dist_sqrd < 16.0f32.powi(2)
77 && rng.random::<f32>() < 0.02
78 {
79 controller.push_basic_input(InputKind::Roll);
80 }
81 }
82 }
83
84 pub fn handle_simple_flying_melee(
87 &self,
88 _agent: &mut Agent,
89 controller: &mut Controller,
90 attack_data: &AttackData,
91 tgt_data: &TargetData,
92 read_data: &ReadData,
93 _rng: &mut impl RngExt,
94 ) {
95 let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
97 .try_normalized()
98 .unwrap_or_else(Vec3::zero);
99 let speed = 1.0;
100 controller.inputs.move_dir = dir_to_target.xy() * speed;
101
102 controller.push_basic_input(InputKind::Fly);
104 if self.physics_state.on_ground.is_some() {
108 controller.push_basic_input(InputKind::Jump);
109 } else {
110 let mut maintain_altitude = |set_point| {
113 let alt = read_data
114 .terrain
115 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
116 .until(Block::is_solid)
117 .cast()
118 .0;
119 let error = set_point - alt;
120 controller.inputs.move_z = error;
121 };
122 if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
123 maintain_altitude(5.0);
124 } else {
125 maintain_altitude(2.0);
126
127 if attack_data.dist_sqrd < 3.5_f32.powi(2) && attack_data.angle < 150.0 {
129 controller.push_basic_input(InputKind::Primary);
130 }
131 }
132 }
133 }
134
135 pub fn handle_bloodmoon_bat_attack(
136 &self,
137 agent: &mut Agent,
138 controller: &mut Controller,
139 attack_data: &AttackData,
140 tgt_data: &TargetData,
141 read_data: &ReadData,
142 _rng: &mut impl RngExt,
143 ) {
144 enum ActionStateTimers {
145 AttackTimer,
146 }
147
148 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
149
150 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
151 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
152 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
154 }
155
156 let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
157 .try_normalized()
158 .unwrap_or_else(Vec3::zero);
159 let speed = 1.0;
160 controller.inputs.move_dir = dir_to_target.xy() * speed;
161
162 controller.push_basic_input(InputKind::Fly);
164 if self.physics_state.on_ground.is_some() {
165 controller.push_basic_input(InputKind::Jump);
166 } else {
167 let mut maintain_altitude = |set_point| {
170 let alt = read_data
171 .terrain
172 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
173 .until(Block::is_solid)
174 .cast()
175 .0;
176 let error = set_point - alt;
177 controller.inputs.move_z = error;
178 };
179 if !(-20.6..20.6).contains(&(tgt_data.pos.0.y - home.y))
180 || !(-26.6..26.6).contains(&(tgt_data.pos.0.x - home.x))
181 {
182 if (home - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
183 controller.push_action(ControlAction::StartInput {
184 input: InputKind::Ability(0),
185 target_entity: None,
186 select_pos: Some(home),
187 });
188 } else {
189 controller.push_basic_input(InputKind::Ability(1));
190 }
191 } else if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (5.0_f32).powi(2) {
192 maintain_altitude(5.0);
193 } else {
194 maintain_altitude(2.0);
195 if tgt_data.pos.0.z < home.z + 5.0 && self.pos.0.z < home.z + 25.0 {
196 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
197 controller.push_basic_input(InputKind::Secondary);
198 } else {
199 controller.push_basic_input(InputKind::Ability(1));
200 }
201 } else if attack_data.dist_sqrd < 6.0_f32.powi(2) {
202 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 2.0 {
204 controller.push_basic_input(InputKind::Ability(2));
205 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize]
206 < 4.0
207 {
208 controller.push_basic_input(InputKind::Ability(3));
209 } else {
210 controller.push_basic_input(InputKind::Primary);
211 }
212 } else if tgt_data.pos.0.z < home.z + 30.0
213 && agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0
214 {
215 controller.push_action(ControlAction::StartInput {
216 input: InputKind::Ability(0),
217 target_entity: agent
218 .target
219 .as_ref()
220 .and_then(|t| read_data.uids.get(t.target))
221 .copied(),
222 select_pos: None,
223 });
224 }
225 }
226 }
227 }
228
229 pub fn handle_vampire_bat_attack(
230 &self,
231 agent: &mut Agent,
232 controller: &mut Controller,
233 _attack_data: &AttackData,
234 _tgt_data: &TargetData,
235 read_data: &ReadData,
236 _rng: &mut impl RngExt,
237 ) {
238 enum ActionStateTimers {
239 AttackTimer,
240 }
241
242 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
243 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 9.0 {
244 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
246 }
247
248 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
250 self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
251 if (home - self.pos.0).xy().magnitude_squared() > (10.0_f32).powi(2) {
253 controller.push_action(ControlAction::StartInput {
254 input: InputKind::Ability(1),
255 target_entity: None,
256 select_pos: Some(home),
257 });
258 }
259 controller.push_basic_input(InputKind::Fly);
261 if self.pos.0.z < home.z + 4.0
262 && agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 6.0
263 {
264 controller.push_basic_input(InputKind::Secondary);
265 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0
266 && (self.pos.0.z - home.z) < 110.0
267 {
268 controller.push_basic_input(InputKind::Primary);
269 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 6.0 {
270 controller.push_basic_input(InputKind::Ability(0));
271 }
272 }
273
274 pub fn handle_bloodmoon_heiress_attack(
275 &self,
276 agent: &mut Agent,
277 controller: &mut Controller,
278 attack_data: &AttackData,
279 tgt_data: &TargetData,
280 read_data: &ReadData,
281 rng: &mut impl RngExt,
282 ) {
283 const DASH_TIMER: usize = 0;
284 const SUMMON_THRESHOLD: f32 = 0.20;
285 enum ActionStateFCounters {
286 FCounterHealthThreshold = 0,
287 }
288 enum ActionStateConditions {
289 ConditionCounterInit = 0,
290 }
291 agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
292 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
293 let line_of_sight_with_target = || {
294 entities_have_line_of_sight(
295 self.pos,
296 self.body,
297 self.scale,
298 tgt_data.pos,
299 tgt_data.body,
300 tgt_data.scale,
301 read_data,
302 )
303 };
304 if !agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] {
307 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
308 1.0 - SUMMON_THRESHOLD;
309 agent.combat_state.conditions[ActionStateConditions::ConditionCounterInit as usize] =
310 true;
311 }
312
313 if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
314 > health_fraction
315 {
316 controller.push_basic_input(InputKind::Ability(2));
318
319 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
320 {
321 agent.combat_state.counters
322 [ActionStateFCounters::FCounterHealthThreshold as usize] -= SUMMON_THRESHOLD;
323 }
324 }
325 else if self
327 .path_toward_target(
328 agent,
329 controller,
330 tgt_data.pos.0,
331 read_data,
332 Path::Separate,
333 None,
334 )
335 .is_none()
336 || !(-3.0..3.0).contains(&(tgt_data.pos.0.z - self.pos.0.z))
337 {
338 controller.push_action(ControlAction::StartInput {
339 input: InputKind::Ability(0),
340 target_entity: agent
341 .target
342 .as_ref()
343 .and_then(|t| read_data.uids.get(t.target))
344 .copied(),
345 select_pos: None,
346 });
347 } else if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
348 {
349 controller.push_basic_input(InputKind::Secondary);
350 } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
351 if agent.combat_state.timers[DASH_TIMER] > 2.0 {
352 agent.combat_state.timers[DASH_TIMER] = 0.0;
353 }
354 match rng.random_range(0..2) {
355 0 => controller.push_basic_input(InputKind::Primary),
356 _ => controller.push_basic_input(InputKind::Ability(3)),
357 };
358 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
359 && self
360 .path_toward_target(
361 agent,
362 controller,
363 tgt_data.pos.0,
364 read_data,
365 Path::Separate,
366 None,
367 )
368 .is_some()
369 && line_of_sight_with_target()
370 && agent.combat_state.timers[DASH_TIMER] > 4.0
371 && attack_data.angle < 45.0
372 {
373 match rng.random_range(0..2) {
374 0 => controller.push_basic_input(InputKind::Secondary),
375 _ => controller.push_basic_input(InputKind::Ability(1)),
376 };
377 agent.combat_state.timers[DASH_TIMER] = 0.0;
378 } else {
379 self.path_toward_target(
380 agent,
381 controller,
382 tgt_data.pos.0,
383 read_data,
384 Path::AtTarget,
385 None,
386 );
387 }
388 }
389
390 pub fn handle_simple_backstab(
394 &self,
395 agent: &mut Agent,
396 controller: &mut Controller,
397 attack_data: &AttackData,
398 tgt_data: &TargetData,
399 read_data: &ReadData,
400 ) {
401 const STRAFE_DIST: f32 = 4.5;
403 const STRAFE_SPEED_MULT: f32 = 0.75;
404 const STRAFE_SPIRAL_MULT: f32 = 0.8; const BACKSTAB_SPEED_MULT: f32 = 0.3;
406
407 let target_ori = agent
409 .target
410 .and_then(|t| read_data.orientations.get(t.target))
411 .map(|ori| ori.look_vec())
412 .unwrap_or_default();
413 let dist = attack_data.dist_sqrd.sqrt();
414 let in_front_of_target = target_ori.dot(self.pos.0 - tgt_data.pos.0) > 0.0;
415
416 if attack_data.in_min_range() && attack_data.angle < 30.0 {
418 controller.push_basic_input(InputKind::Primary);
419 controller.inputs.move_dir = Vec2::zero();
420 }
421
422 if attack_data.dist_sqrd < STRAFE_DIST.powi(2) {
423 let vec_to_target = (tgt_data.pos.0 - self.pos.0).xy();
426 if in_front_of_target {
427 let theta = (PI / 2. - dist * 0.1).max(0.0);
428 let potential_move_dirs = [
430 vec_to_target
431 .rotated_z(theta)
432 .try_normalized()
433 .unwrap_or_default(),
434 vec_to_target
435 .rotated_z(-theta)
436 .try_normalized()
437 .unwrap_or_default(),
438 ];
439 if let Some(move_dir) = potential_move_dirs
441 .iter()
442 .find(|move_dir| target_ori.xy().dot(**move_dir) < 0.0)
443 {
444 controller.inputs.move_dir =
445 STRAFE_SPEED_MULT * (*move_dir - STRAFE_SPIRAL_MULT * target_ori.xy());
446 }
447 } else {
448 let move_target = tgt_data.pos.0.xy() - dist / 2. * target_ori.xy();
451 controller.inputs.move_dir = ((move_target - self.pos.0) * BACKSTAB_SPEED_MULT)
452 .try_normalized()
453 .unwrap_or_default();
454 }
455 } else {
456 self.path_toward_target(
457 agent,
458 controller,
459 tgt_data.pos.0,
460 read_data,
461 Path::AtTarget,
462 None,
463 );
464 }
465 }
466
467 pub fn handle_elevated_ranged(
468 &self,
469 agent: &mut Agent,
470 controller: &mut Controller,
471 attack_data: &AttackData,
472 tgt_data: &TargetData,
473 read_data: &ReadData,
474 ) {
475 const PREF_DIST: f32 = 30.0;
477 const RETREAT_DIST: f32 = 8.0;
478
479 let line_of_sight_with_target = || {
480 entities_have_line_of_sight(
481 self.pos,
482 self.body,
483 self.scale,
484 tgt_data.pos,
485 tgt_data.body,
486 tgt_data.scale,
487 read_data,
488 )
489 };
490 let elevation = self.pos.0.z - tgt_data.pos.0.z;
491
492 if attack_data.angle_xy < 30.0
493 && (elevation > 10.0 || attack_data.dist_sqrd > PREF_DIST.powi(2))
494 && line_of_sight_with_target()
495 {
496 controller.push_basic_input(InputKind::Primary);
497 } else if attack_data.dist_sqrd < RETREAT_DIST.powi(2) {
498 if let Some((bearing, _, stuck)) = agent.chaser.chase(
500 &*read_data.terrain,
501 self.pos.0,
502 self.vel.0,
503 tgt_data.pos.0,
504 TraversalConfig {
505 min_tgt_dist: 1.25,
506 ..self.traversal_config
507 },
508 &read_data.time,
509 ) {
510 let flee_dir = -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero);
511 let pos = self.pos.0.xy().with_z(self.pos.0.z + 1.5);
512 if read_data
513 .terrain
514 .ray(pos, pos + flee_dir * 2.0)
515 .until(|b| b.is_solid() || b.get_sprite().is_none())
516 .cast()
517 .0
518 > 1.0
519 {
520 controller.inputs.move_dir = flee_dir;
522 if !self.char_state.is_attack() {
523 self.unstuck_if(stuck, controller);
524 controller.inputs.look_dir = -controller.inputs.look_dir;
525 }
526 } else {
527 controller.push_basic_input(InputKind::Primary);
529 }
530 }
531 } else if attack_data.dist_sqrd < PREF_DIST.powi(2) {
532 if let Some((bearing, _, stuck)) = agent.chaser.chase(
534 &*read_data.terrain,
535 self.pos.0,
536 self.vel.0,
537 tgt_data.pos.0,
538 TraversalConfig {
539 min_tgt_dist: 1.25,
540 ..self.traversal_config
541 },
542 &read_data.time,
543 ) {
544 if line_of_sight_with_target() {
545 controller.push_basic_input(InputKind::Primary);
546 }
547 self.unstuck_if(stuck, controller);
548 controller.inputs.move_dir =
549 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero);
550 }
551 } else {
552 self.path_toward_target(
553 agent,
554 controller,
555 tgt_data.pos.0,
556 read_data,
557 Path::AtTarget,
558 None,
559 );
560 }
561 }
562
563 pub fn handle_hammer_attack(
564 &self,
565 agent: &mut Agent,
566 controller: &mut Controller,
567 attack_data: &AttackData,
568 tgt_data: &TargetData,
569 read_data: &ReadData,
570 rng: &mut impl RngExt,
571 ) {
572 if !agent.combat_state.initialized {
573 agent.combat_state.initialized = true;
574 let available_tactics = {
575 let mut tactics = Vec::new();
576 let try_tactic = |skill, tactic, tactics: &mut Vec<HammerTactics>| {
577 if self.skill_set.has_skill(Skill::Hammer(skill)) {
578 tactics.push(tactic);
579 }
580 };
581 try_tactic(
582 HammerSkill::Thunderclap,
583 HammerTactics::AttackExpert,
584 &mut tactics,
585 );
586 try_tactic(
587 HammerSkill::Judgement,
588 HammerTactics::SupportExpert,
589 &mut tactics,
590 );
591 if tactics.is_empty() {
592 try_tactic(
593 HammerSkill::IronTempest,
594 HammerTactics::AttackAdvanced,
595 &mut tactics,
596 );
597 try_tactic(
598 HammerSkill::Rampart,
599 HammerTactics::SupportAdvanced,
600 &mut tactics,
601 );
602 }
603 if tactics.is_empty() {
604 try_tactic(
605 HammerSkill::Retaliate,
606 HammerTactics::AttackIntermediate,
607 &mut tactics,
608 );
609 try_tactic(
610 HammerSkill::PileDriver,
611 HammerTactics::SupportIntermediate,
612 &mut tactics,
613 );
614 }
615 if tactics.is_empty() {
616 try_tactic(
617 HammerSkill::Tremor,
618 HammerTactics::AttackSimple,
619 &mut tactics,
620 );
621 try_tactic(
622 HammerSkill::HeavyWhorl,
623 HammerTactics::SupportSimple,
624 &mut tactics,
625 );
626 }
627 if tactics.is_empty() {
628 try_tactic(
629 HammerSkill::ScornfulSwipe,
630 HammerTactics::Simple,
631 &mut tactics,
632 );
633 }
634 if tactics.is_empty() {
635 tactics.push(HammerTactics::Unskilled);
636 }
637 tactics
638 };
639
640 let tactic = available_tactics
641 .choose(rng)
642 .copied()
643 .unwrap_or(HammerTactics::Unskilled);
644
645 agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
646
647 let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
648 let set_ability = |controller: &mut Controller, slot, skill| {
649 controller.push_event(ControlEvent::ChangeAbility {
650 slot,
651 auxiliary_key,
652 new_ability: AuxiliaryAbility::MainWeapon(skill),
653 });
654 };
655 let mut set_random = |controller: &mut Controller, slot, options: &mut Vec<usize>| {
656 if options.is_empty() {
657 return;
658 }
659 let i = rng.random_range(0..options.len());
660 set_ability(controller, slot, options.swap_remove(i));
661 };
662
663 match tactic {
664 HammerTactics::Unskilled => {},
665 HammerTactics::Simple => {
666 set_ability(controller, 0, 0);
668 },
669 HammerTactics::AttackSimple => {
670 set_ability(controller, 0, 0);
672 set_ability(controller, 1, rng.random_range(1..3));
674 },
675 HammerTactics::AttackIntermediate => {
676 set_ability(controller, 0, 0);
678 set_ability(controller, 1, rng.random_range(1..3));
680 set_ability(controller, 2, rng.random_range(3..6));
682 },
683 HammerTactics::AttackAdvanced => {
684 let mut options = vec![0, 1, 2, 3, 4, 5];
686 set_random(controller, 0, &mut options);
687 set_random(controller, 1, &mut options);
688 set_random(controller, 2, &mut options);
689 set_ability(controller, 3, rng.random_range(6..8));
690 },
691 HammerTactics::AttackExpert => {
692 let mut options = vec![0, 1, 2, 3, 4, 5, 6, 7];
695 set_random(controller, 0, &mut options);
696 set_random(controller, 1, &mut options);
697 set_random(controller, 2, &mut options);
698 set_random(controller, 3, &mut options);
699 set_ability(controller, 4, rng.random_range(8..10));
700 },
701 HammerTactics::SupportSimple => {
702 set_ability(controller, 0, 0);
704 set_ability(controller, 1, rng.random_range(10..12));
706 },
707 HammerTactics::SupportIntermediate => {
708 set_ability(controller, 0, 0);
710 set_ability(controller, 1, rng.random_range(10..12));
712 set_ability(controller, 2, rng.random_range(12..15));
714 },
715 HammerTactics::SupportAdvanced => {
716 let mut options = vec![0, 10, 11, 12, 13, 14];
719 set_random(controller, 0, &mut options);
720 set_random(controller, 1, &mut options);
721 set_random(controller, 2, &mut options);
722 set_ability(controller, 3, rng.random_range(15..17));
723 },
724 HammerTactics::SupportExpert => {
725 let mut options = vec![0, 10, 11, 12, 13, 14, 15, 16];
728 set_random(controller, 0, &mut options);
729 set_random(controller, 1, &mut options);
730 set_random(controller, 2, &mut options);
731 set_random(controller, 3, &mut options);
732 set_ability(controller, 4, rng.random_range(17..19));
733 },
734 }
735
736 agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
737 ActionMode::Reckless as u8;
738 }
739
740 enum IntCounters {
741 Tactic = 0,
742 ActionMode = 1,
743 }
744
745 enum Timers {
746 GuardedCycle = 0,
747 PosTimeOut = 1,
748 }
749
750 enum Conditions {
751 GuardedDefend = 0,
752 RollingBreakThrough = 1,
753 }
754
755 enum FloatCounters {
756 GuardedTimer = 0,
757 }
758
759 enum Positions {
760 GuardedCover = 0,
761 Flee = 1,
762 }
763
764 let attempt_attack = handle_attack_aggression(
765 self,
766 agent,
767 controller,
768 attack_data,
769 tgt_data,
770 read_data,
771 rng,
772 Timers::PosTimeOut as usize,
773 Timers::GuardedCycle as usize,
774 FloatCounters::GuardedTimer as usize,
775 IntCounters::ActionMode as usize,
776 Conditions::GuardedDefend as usize,
777 Conditions::RollingBreakThrough as usize,
778 Positions::GuardedCover as usize,
779 Positions::Flee as usize,
780 );
781
782 let attack_failed = if attempt_attack {
783 let primary = self.extract_ability(AbilityInput::Primary);
784 let secondary = self.extract_ability(AbilityInput::Secondary);
785 let abilities = [
786 self.extract_ability(AbilityInput::Auxiliary(0)),
787 self.extract_ability(AbilityInput::Auxiliary(1)),
788 self.extract_ability(AbilityInput::Auxiliary(2)),
789 self.extract_ability(AbilityInput::Auxiliary(3)),
790 self.extract_ability(AbilityInput::Auxiliary(4)),
791 ];
792 let could_use_input = |input, desired_energy| match input {
793 InputKind::Primary => primary.as_ref().is_some_and(|p| {
794 p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
795 }),
796 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
797 s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
798 }),
799 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
800 let ability = self.active_abilities.get_ability(
801 AbilityInput::Auxiliary(x),
802 Some(self.inventory),
803 Some(self.skill_set),
804 self.stats,
805 );
806 let additional_conditions = match ability {
807 Ability::MainWeaponAux(0) => self
808 .buffs
809 .is_some_and(|buffs| !buffs.contains(BuffKind::ScornfulTaunt)),
810 Ability::MainWeaponAux(2) => {
811 tgt_data.char_state.is_some_and(|cs| cs.is_stunned())
812 },
813 Ability::MainWeaponAux(4) => tgt_data.ori.is_some_and(|ori| {
814 ori.look_vec().angle_between(tgt_data.pos.0 - self.pos.0)
815 < combat::BEHIND_TARGET_ANGLE
816 }),
817 Ability::MainWeaponAux(5) => tgt_data.char_state.is_some_and(|cs| {
818 cs.is_block(AttackSource::Melee) || cs.is_parry(AttackSource::Melee)
819 }),
820 Ability::MainWeaponAux(7) => tgt_data
821 .buffs
822 .is_some_and(|buffs| !buffs.contains(BuffKind::OffBalance)),
823 Ability::MainWeaponAux(12) => tgt_data
824 .buffs
825 .is_some_and(|buffs| !buffs.contains(BuffKind::Rooted)),
826 Ability::MainWeaponAux(13) => tgt_data
827 .buffs
828 .is_some_and(|buffs| !buffs.contains(BuffKind::Winded)),
829 Ability::MainWeaponAux(14) => tgt_data
830 .buffs
831 .is_some_and(|buffs| !buffs.contains(BuffKind::Amnesia)),
832 Ability::MainWeaponAux(15) => self
833 .buffs
834 .is_some_and(|buffs| !buffs.contains(BuffKind::ProtectingWard)),
835 _ => true,
836 };
837 a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
838 && additional_conditions
839 }),
840 _ => false,
841 };
842 let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
843 if matches!(current_input, InputKind::Secondary) {
844 let charging =
845 matches!(self.char_state.stage_section(), Some(StageSection::Charge));
846 let charged = self
847 .char_state
848 .durations()
849 .and_then(|durs| durs.charge)
850 .zip(self.char_state.timer())
851 .is_some_and(|(dur, timer)| timer > dur);
852 if !(charging && charged) {
853 *next_input = Some(InputKind::Secondary);
854 }
855 } else {
856 *next_input = Some(current_input);
857 }
858 };
859 let current_input = self.char_state.ability_info().map(|ai| ai.input);
860 let ability_preferences = AbilityPreferences {
861 desired_energy: 40.0,
862 combo_scaling_buildup: 0,
863 };
864 let mut next_input = None;
865 if let Some(input) = current_input {
866 continue_current_input(input, &mut next_input);
867 } else {
868 match HammerTactics::from_u8(
869 agent.combat_state.int_counters[IntCounters::Tactic as usize],
870 ) {
871 HammerTactics::Unskilled => {
872 if rng.random_bool(0.5) {
873 next_input = Some(InputKind::Primary);
874 } else {
875 next_input = Some(InputKind::Secondary);
876 }
877 },
878 HammerTactics::Simple => {
879 if rng.random_bool(0.5) {
880 next_input = Some(InputKind::Primary);
881 } else {
882 next_input = Some(InputKind::Secondary);
883 }
884 },
885 HammerTactics::AttackSimple | HammerTactics::SupportSimple => {
886 if could_use_input(InputKind::Ability(0), ability_preferences) {
887 next_input = Some(InputKind::Ability(0));
888 } else if rng.random_bool(0.5) {
889 next_input = Some(InputKind::Primary);
890 } else {
891 next_input = Some(InputKind::Secondary);
892 }
893 },
894 HammerTactics::AttackIntermediate | HammerTactics::SupportIntermediate => {
895 let random_ability = InputKind::Ability(rng.random_range(0..3));
896 if could_use_input(random_ability, ability_preferences) {
897 next_input = Some(random_ability);
898 } else if rng.random_bool(0.5) {
899 next_input = Some(InputKind::Primary);
900 } else {
901 next_input = Some(InputKind::Secondary);
902 }
903 },
904 HammerTactics::AttackAdvanced | HammerTactics::SupportAdvanced => {
905 let random_ability = InputKind::Ability(rng.random_range(0..5));
906 if could_use_input(random_ability, ability_preferences) {
907 next_input = Some(random_ability);
908 } else if rng.random_bool(0.5) {
909 next_input = Some(InputKind::Primary);
910 } else {
911 next_input = Some(InputKind::Secondary);
912 }
913 },
914 HammerTactics::AttackExpert | HammerTactics::SupportExpert => {
915 let random_ability = InputKind::Ability(rng.random_range(0..5));
916 if could_use_input(random_ability, ability_preferences) {
917 next_input = Some(random_ability);
918 } else if rng.random_bool(0.5) {
919 next_input = Some(InputKind::Primary);
920 } else {
921 next_input = Some(InputKind::Secondary);
922 }
923 },
924 }
925 }
926 if let Some(input) = next_input {
927 if could_use_input(input, ability_preferences) {
928 controller.push_basic_input(input);
929 false
930 } else {
931 true
932 }
933 } else {
934 true
935 }
936 } else {
937 false
938 };
939
940 if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
941 self.path_toward_target(
942 agent,
943 controller,
944 tgt_data.pos.0,
945 read_data,
946 Path::Separate,
947 None,
948 );
949 }
950 }
951
952 pub fn handle_sword_attack(
953 &self,
954 agent: &mut Agent,
955 controller: &mut Controller,
956 attack_data: &AttackData,
957 tgt_data: &TargetData,
958 read_data: &ReadData,
959 rng: &mut impl RngExt,
960 ) {
961 if !agent.combat_state.initialized {
962 agent.combat_state.initialized = true;
963 let available_tactics = {
964 let mut tactics = Vec::new();
965 let try_tactic = |skill, tactic, tactics: &mut Vec<SwordTactics>| {
966 if self.skill_set.has_skill(Skill::Sword(skill)) {
967 tactics.push(tactic);
968 }
969 };
970 try_tactic(
971 SwordSkill::HeavyFortitude,
972 SwordTactics::HeavyAdvanced,
973 &mut tactics,
974 );
975 try_tactic(
976 SwordSkill::AgileDancingEdge,
977 SwordTactics::AgileAdvanced,
978 &mut tactics,
979 );
980 try_tactic(
981 SwordSkill::DefensiveStalwartSword,
982 SwordTactics::DefensiveAdvanced,
983 &mut tactics,
984 );
985 try_tactic(
986 SwordSkill::CripplingEviscerate,
987 SwordTactics::CripplingAdvanced,
988 &mut tactics,
989 );
990 try_tactic(
991 SwordSkill::CleavingBladeFever,
992 SwordTactics::CleavingAdvanced,
993 &mut tactics,
994 );
995 if tactics.is_empty() {
996 try_tactic(
997 SwordSkill::HeavySweep,
998 SwordTactics::HeavySimple,
999 &mut tactics,
1000 );
1001 try_tactic(
1002 SwordSkill::AgileQuickDraw,
1003 SwordTactics::AgileSimple,
1004 &mut tactics,
1005 );
1006 try_tactic(
1007 SwordSkill::DefensiveDisengage,
1008 SwordTactics::DefensiveSimple,
1009 &mut tactics,
1010 );
1011 try_tactic(
1012 SwordSkill::CripplingGouge,
1013 SwordTactics::CripplingSimple,
1014 &mut tactics,
1015 );
1016 try_tactic(
1017 SwordSkill::CleavingWhirlwindSlice,
1018 SwordTactics::CleavingSimple,
1019 &mut tactics,
1020 );
1021 }
1022 if tactics.is_empty() {
1023 try_tactic(SwordSkill::CrescentSlash, SwordTactics::Basic, &mut tactics);
1024 }
1025 if tactics.is_empty() {
1026 tactics.push(SwordTactics::Unskilled);
1027 }
1028 tactics
1029 };
1030
1031 let tactic = available_tactics
1032 .choose(rng)
1033 .copied()
1034 .unwrap_or(SwordTactics::Unskilled);
1035
1036 agent.combat_state.int_counters[IntCounters::Tactics as usize] = tactic as u8;
1037
1038 let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
1039 let set_sword_ability = |controller: &mut Controller, slot, skill| {
1040 controller.push_event(ControlEvent::ChangeAbility {
1041 slot,
1042 auxiliary_key,
1043 new_ability: AuxiliaryAbility::MainWeapon(skill),
1044 });
1045 };
1046
1047 match tactic {
1048 SwordTactics::Unskilled => {},
1049 SwordTactics::Basic => {
1050 set_sword_ability(controller, 0, 0);
1052 set_sword_ability(controller, 1, 1);
1054 set_sword_ability(controller, 2, 2);
1056 set_sword_ability(controller, 3, 3);
1058 set_sword_ability(controller, 4, 4);
1060 },
1061 SwordTactics::HeavySimple => {
1062 set_sword_ability(controller, 0, 5);
1064 set_sword_ability(controller, 1, 0);
1066 set_sword_ability(controller, 2, 3);
1068 set_sword_ability(controller, 3, 6);
1070 set_sword_ability(controller, 4, 7);
1072 },
1073 SwordTactics::AgileSimple => {
1074 set_sword_ability(controller, 0, 5);
1076 set_sword_ability(controller, 1, 2);
1078 set_sword_ability(controller, 2, 4);
1080 set_sword_ability(controller, 3, 8);
1082 set_sword_ability(controller, 4, 9);
1084 },
1085 SwordTactics::DefensiveSimple => {
1086 set_sword_ability(controller, 0, 5);
1088 set_sword_ability(controller, 1, 0);
1090 set_sword_ability(controller, 2, 1);
1092 set_sword_ability(controller, 3, 10);
1094 set_sword_ability(controller, 4, 11);
1096 },
1097 SwordTactics::CripplingSimple => {
1098 set_sword_ability(controller, 0, 5);
1100 set_sword_ability(controller, 1, 1);
1102 set_sword_ability(controller, 2, 2);
1104 set_sword_ability(controller, 3, 12);
1106 set_sword_ability(controller, 4, 13);
1108 },
1109 SwordTactics::CleavingSimple => {
1110 set_sword_ability(controller, 0, 5);
1112 set_sword_ability(controller, 1, 3);
1114 set_sword_ability(controller, 2, 4);
1116 set_sword_ability(controller, 3, 14);
1118 set_sword_ability(controller, 4, 15);
1120 },
1121 SwordTactics::HeavyAdvanced => {
1122 set_sword_ability(controller, 0, 5);
1124 set_sword_ability(controller, 1, 6);
1126 set_sword_ability(controller, 2, 7);
1128 set_sword_ability(controller, 3, 16);
1130 set_sword_ability(controller, 4, 17);
1132 },
1133 SwordTactics::AgileAdvanced => {
1134 set_sword_ability(controller, 0, 5);
1136 set_sword_ability(controller, 1, 8);
1138 set_sword_ability(controller, 2, 9);
1140 set_sword_ability(controller, 3, 18);
1142 set_sword_ability(controller, 4, 19);
1144 },
1145 SwordTactics::DefensiveAdvanced => {
1146 set_sword_ability(controller, 0, 5);
1148 set_sword_ability(controller, 1, 10);
1150 set_sword_ability(controller, 2, 11);
1152 set_sword_ability(controller, 3, 20);
1154 set_sword_ability(controller, 4, 21);
1156 },
1157 SwordTactics::CripplingAdvanced => {
1158 set_sword_ability(controller, 0, 5);
1160 set_sword_ability(controller, 1, 12);
1162 set_sword_ability(controller, 2, 13);
1164 set_sword_ability(controller, 3, 22);
1166 set_sword_ability(controller, 4, 23);
1168 },
1169 SwordTactics::CleavingAdvanced => {
1170 set_sword_ability(controller, 0, 5);
1172 set_sword_ability(controller, 1, 14);
1174 set_sword_ability(controller, 2, 15);
1176 set_sword_ability(controller, 3, 24);
1178 set_sword_ability(controller, 4, 25);
1180 },
1181 }
1182
1183 agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
1184 ActionMode::Reckless as u8;
1185 }
1186
1187 enum IntCounters {
1188 Tactics = 0,
1189 ActionMode = 1,
1190 }
1191
1192 enum Timers {
1193 GuardedCycle = 0,
1194 PosTimeOut = 1,
1195 }
1196
1197 enum Conditions {
1198 GuardedDefend = 0,
1199 RollingBreakThrough = 1,
1200 }
1201
1202 enum FloatCounters {
1203 GuardedTimer = 0,
1204 }
1205
1206 enum Positions {
1207 GuardedCover = 0,
1208 Flee = 1,
1209 }
1210
1211 let attempt_attack = handle_attack_aggression(
1212 self,
1213 agent,
1214 controller,
1215 attack_data,
1216 tgt_data,
1217 read_data,
1218 rng,
1219 Timers::PosTimeOut as usize,
1220 Timers::GuardedCycle as usize,
1221 FloatCounters::GuardedTimer as usize,
1222 IntCounters::ActionMode as usize,
1223 Conditions::GuardedDefend as usize,
1224 Conditions::RollingBreakThrough as usize,
1225 Positions::GuardedCover as usize,
1226 Positions::Flee as usize,
1227 );
1228
1229 let attack_failed = if attempt_attack {
1230 let primary = self.extract_ability(AbilityInput::Primary);
1231 let secondary = self.extract_ability(AbilityInput::Secondary);
1232 let abilities = [
1233 self.extract_ability(AbilityInput::Auxiliary(0)),
1234 self.extract_ability(AbilityInput::Auxiliary(1)),
1235 self.extract_ability(AbilityInput::Auxiliary(2)),
1236 self.extract_ability(AbilityInput::Auxiliary(3)),
1237 self.extract_ability(AbilityInput::Auxiliary(4)),
1238 ];
1239 let could_use_input = |input, desired_energy| match input {
1240 InputKind::Primary => primary.as_ref().is_some_and(|p| {
1241 p.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1242 }),
1243 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
1244 s.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1245 }),
1246 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
1247 a.could_use(attack_data, self, tgt_data, read_data, desired_energy)
1248 }),
1249 _ => false,
1250 };
1251 let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
1252 if matches!(current_input, InputKind::Secondary) {
1253 let charging =
1254 matches!(self.char_state.stage_section(), Some(StageSection::Charge));
1255 let charged = self
1256 .char_state
1257 .durations()
1258 .and_then(|durs| durs.charge)
1259 .zip(self.char_state.timer())
1260 .is_some_and(|(dur, timer)| timer > dur);
1261 if !(charging && charged) {
1262 *next_input = Some(InputKind::Secondary);
1263 }
1264 } else {
1265 *next_input = Some(current_input);
1266 }
1267 };
1268 match SwordTactics::from_u8(
1269 agent.combat_state.int_counters[IntCounters::Tactics as usize],
1270 ) {
1271 SwordTactics::Unskilled => {
1272 let ability_preferences = AbilityPreferences {
1273 desired_energy: 15.0,
1274 combo_scaling_buildup: 0,
1275 };
1276 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1277 let mut next_input = None;
1278 if let Some(input) = current_input {
1279 continue_current_input(input, &mut next_input);
1280 } else if rng.random_bool(0.5) {
1281 next_input = Some(InputKind::Primary);
1282 } else {
1283 next_input = Some(InputKind::Secondary);
1284 };
1285 if let Some(input) = next_input {
1286 if could_use_input(input, ability_preferences) {
1287 controller.push_basic_input(input);
1288 false
1289 } else {
1290 true
1291 }
1292 } else {
1293 true
1294 }
1295 },
1296 SwordTactics::Basic => {
1297 let ability_preferences = AbilityPreferences {
1298 desired_energy: 25.0,
1299 combo_scaling_buildup: 0,
1300 };
1301 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1302 let mut next_input = None;
1303 if let Some(input) = current_input {
1304 continue_current_input(input, &mut next_input);
1305 } else {
1306 let attempt_ability = InputKind::Ability(rng.random_range(0..5));
1307 if could_use_input(attempt_ability, ability_preferences) {
1308 next_input = Some(attempt_ability);
1309 } else if rng.random_bool(0.5) {
1310 next_input = Some(InputKind::Primary);
1311 } else {
1312 next_input = Some(InputKind::Secondary);
1313 }
1314 };
1315 if let Some(input) = next_input {
1316 if could_use_input(input, ability_preferences) {
1317 controller.push_basic_input(input);
1318 false
1319 } else {
1320 true
1321 }
1322 } else {
1323 true
1324 }
1325 },
1326 SwordTactics::HeavySimple => {
1327 let ability_preferences = AbilityPreferences {
1328 desired_energy: 35.0,
1329 combo_scaling_buildup: 0,
1330 };
1331 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1332 let mut next_input = None;
1333 if let Some(input) = current_input {
1334 continue_current_input(input, &mut next_input);
1335 } else {
1336 let stance_ability = InputKind::Ability(rng.random_range(3..5));
1337 let random_ability = InputKind::Ability(rng.random_range(1..5));
1338 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) {
1339 if could_use_input(stance_ability, ability_preferences) {
1340 next_input = Some(stance_ability);
1341 } else if rng.random_bool(0.5) {
1342 next_input = Some(InputKind::Primary);
1343 } else {
1344 next_input = Some(InputKind::Secondary);
1345 }
1346 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1347 next_input = Some(InputKind::Ability(0));
1348 } else if could_use_input(random_ability, ability_preferences) {
1349 next_input = Some(random_ability);
1350 } else if rng.random_bool(0.5) {
1351 next_input = Some(InputKind::Primary);
1352 } else {
1353 next_input = Some(InputKind::Secondary);
1354 }
1355 };
1356 if let Some(input) = next_input {
1357 if could_use_input(input, ability_preferences) {
1358 controller.push_basic_input(input);
1359 false
1360 } else {
1361 true
1362 }
1363 } else {
1364 true
1365 }
1366 },
1367 SwordTactics::AgileSimple => {
1368 let ability_preferences = AbilityPreferences {
1369 desired_energy: 35.0,
1370 combo_scaling_buildup: 0,
1371 };
1372 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1373 let mut next_input = None;
1374 if let Some(input) = current_input {
1375 continue_current_input(input, &mut next_input);
1376 } else {
1377 let stance_ability = InputKind::Ability(rng.random_range(3..5));
1378 let random_ability = InputKind::Ability(rng.random_range(1..5));
1379 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) {
1380 if could_use_input(stance_ability, ability_preferences) {
1381 next_input = Some(stance_ability);
1382 } else if rng.random_bool(0.5) {
1383 next_input = Some(InputKind::Primary);
1384 } else {
1385 next_input = Some(InputKind::Secondary);
1386 }
1387 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1388 next_input = Some(InputKind::Ability(0));
1389 } else if could_use_input(random_ability, ability_preferences) {
1390 next_input = Some(random_ability);
1391 } else if rng.random_bool(0.5) {
1392 next_input = Some(InputKind::Primary);
1393 } else {
1394 next_input = Some(InputKind::Secondary);
1395 }
1396 };
1397 if let Some(input) = next_input {
1398 if could_use_input(input, ability_preferences) {
1399 controller.push_basic_input(input);
1400 false
1401 } else {
1402 true
1403 }
1404 } else {
1405 true
1406 }
1407 },
1408 SwordTactics::DefensiveSimple => {
1409 let ability_preferences = AbilityPreferences {
1410 desired_energy: 35.0,
1411 combo_scaling_buildup: 0,
1412 };
1413 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1414 let mut next_input = None;
1415 if let Some(input) = current_input {
1416 continue_current_input(input, &mut next_input);
1417 } else {
1418 let stance_ability = InputKind::Ability(rng.random_range(3..5));
1419 let random_ability = InputKind::Ability(rng.random_range(1..5));
1420 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) {
1421 if could_use_input(stance_ability, ability_preferences) {
1422 next_input = Some(stance_ability);
1423 } else if rng.random_bool(0.5) {
1424 next_input = Some(InputKind::Primary);
1425 } else {
1426 next_input = Some(InputKind::Secondary);
1427 }
1428 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1429 next_input = Some(InputKind::Ability(0));
1430 } else if could_use_input(InputKind::Ability(3), ability_preferences) {
1431 next_input = Some(InputKind::Ability(3));
1432 } else if could_use_input(random_ability, ability_preferences) {
1433 next_input = Some(random_ability);
1434 } else if rng.random_bool(0.5) {
1435 next_input = Some(InputKind::Primary);
1436 } else {
1437 next_input = Some(InputKind::Secondary);
1438 }
1439 };
1440 if let Some(input) = next_input {
1441 if could_use_input(input, ability_preferences) {
1442 controller.push_basic_input(input);
1443 false
1444 } else {
1445 true
1446 }
1447 } else {
1448 true
1449 }
1450 },
1451 SwordTactics::CripplingSimple => {
1452 let ability_preferences = AbilityPreferences {
1453 desired_energy: 35.0,
1454 combo_scaling_buildup: 0,
1455 };
1456 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1457 let mut next_input = None;
1458 if let Some(input) = current_input {
1459 continue_current_input(input, &mut next_input);
1460 } else {
1461 let stance_ability = InputKind::Ability(rng.random_range(3..5));
1462 let random_ability = InputKind::Ability(rng.random_range(1..5));
1463 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) {
1464 if could_use_input(stance_ability, ability_preferences) {
1465 next_input = Some(stance_ability);
1466 } else if rng.random_bool(0.5) {
1467 next_input = Some(InputKind::Primary);
1468 } else {
1469 next_input = Some(InputKind::Secondary);
1470 }
1471 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1472 next_input = Some(InputKind::Ability(0));
1473 } else if could_use_input(random_ability, ability_preferences) {
1474 next_input = Some(random_ability);
1475 } else if rng.random_bool(0.5) {
1476 next_input = Some(InputKind::Primary);
1477 } else {
1478 next_input = Some(InputKind::Secondary);
1479 }
1480 };
1481 if let Some(input) = next_input {
1482 if could_use_input(input, ability_preferences) {
1483 controller.push_basic_input(input);
1484 false
1485 } else {
1486 true
1487 }
1488 } else {
1489 true
1490 }
1491 },
1492 SwordTactics::CleavingSimple => {
1493 let ability_preferences = AbilityPreferences {
1494 desired_energy: 35.0,
1495 combo_scaling_buildup: 0,
1496 };
1497 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1498 let mut next_input = None;
1499 if let Some(input) = current_input {
1500 continue_current_input(input, &mut next_input);
1501 } else {
1502 let stance_ability = InputKind::Ability(rng.random_range(3..5));
1503 let random_ability = InputKind::Ability(rng.random_range(1..5));
1504 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) {
1505 if could_use_input(stance_ability, ability_preferences) {
1506 next_input = Some(stance_ability);
1507 } else if rng.random_bool(0.5) {
1508 next_input = Some(InputKind::Primary);
1509 } else {
1510 next_input = Some(InputKind::Secondary);
1511 }
1512 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1513 next_input = Some(InputKind::Ability(0));
1514 } else if could_use_input(random_ability, ability_preferences) {
1515 next_input = Some(random_ability);
1516 } else if rng.random_bool(0.5) {
1517 next_input = Some(InputKind::Primary);
1518 } else {
1519 next_input = Some(InputKind::Secondary);
1520 }
1521 };
1522 if let Some(input) = next_input {
1523 if could_use_input(input, ability_preferences) {
1524 controller.push_basic_input(input);
1525 false
1526 } else {
1527 true
1528 }
1529 } else {
1530 true
1531 }
1532 },
1533 SwordTactics::HeavyAdvanced => {
1534 let ability_preferences = AbilityPreferences {
1535 desired_energy: 50.0,
1536 combo_scaling_buildup: 0,
1537 };
1538 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1539 let mut next_input = None;
1540 if let Some(input) = current_input {
1541 continue_current_input(input, &mut next_input);
1542 } else {
1543 let stance_ability = InputKind::Ability(rng.random_range(1..3));
1544 let random_ability = InputKind::Ability(rng.random_range(1..5));
1545 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Heavy))) {
1546 if could_use_input(stance_ability, ability_preferences) {
1547 next_input = Some(stance_ability);
1548 } else if rng.random_bool(0.5) {
1549 next_input = Some(InputKind::Primary);
1550 } else {
1551 next_input = Some(InputKind::Secondary);
1552 }
1553 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1554 next_input = Some(InputKind::Ability(0));
1555 } else if could_use_input(random_ability, ability_preferences) {
1556 next_input = Some(random_ability);
1557 } else if rng.random_bool(0.5) {
1558 next_input = Some(InputKind::Primary);
1559 } else {
1560 next_input = Some(InputKind::Secondary);
1561 }
1562 };
1563 if let Some(input) = next_input {
1564 if could_use_input(input, ability_preferences) {
1565 controller.push_basic_input(input);
1566 false
1567 } else {
1568 true
1569 }
1570 } else {
1571 true
1572 }
1573 },
1574 SwordTactics::AgileAdvanced => {
1575 let ability_preferences = AbilityPreferences {
1576 desired_energy: 50.0,
1577 combo_scaling_buildup: 0,
1578 };
1579 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1580 let mut next_input = None;
1581 if let Some(input) = current_input {
1582 continue_current_input(input, &mut next_input);
1583 } else {
1584 let stance_ability = InputKind::Ability(rng.random_range(1..3));
1585 let random_ability = InputKind::Ability(rng.random_range(1..5));
1586 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Agile))) {
1587 if could_use_input(stance_ability, ability_preferences) {
1588 next_input = Some(stance_ability);
1589 } else if rng.random_bool(0.5) {
1590 next_input = Some(InputKind::Primary);
1591 } else {
1592 next_input = Some(InputKind::Secondary);
1593 }
1594 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1595 next_input = Some(InputKind::Ability(0));
1596 } else if could_use_input(random_ability, ability_preferences) {
1597 next_input = Some(random_ability);
1598 } else if rng.random_bool(0.5) {
1599 next_input = Some(InputKind::Primary);
1600 } else {
1601 next_input = Some(InputKind::Secondary);
1602 }
1603 };
1604 if let Some(input) = next_input {
1605 if could_use_input(input, ability_preferences) {
1606 controller.push_basic_input(input);
1607 false
1608 } else {
1609 true
1610 }
1611 } else {
1612 true
1613 }
1614 },
1615 SwordTactics::DefensiveAdvanced => {
1616 let ability_preferences = AbilityPreferences {
1617 desired_energy: 50.0,
1618 combo_scaling_buildup: 0,
1619 };
1620 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1621 let mut next_input = None;
1622 if let Some(input) = current_input {
1623 continue_current_input(input, &mut next_input);
1624 } else {
1625 let stance_ability = InputKind::Ability(rng.random_range(1..3));
1626 let random_ability = InputKind::Ability(rng.random_range(1..4));
1627 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Defensive))) {
1628 if could_use_input(stance_ability, ability_preferences) {
1629 next_input = Some(stance_ability);
1630 } else if rng.random_bool(0.5) {
1631 next_input = Some(InputKind::Primary);
1632 } else {
1633 next_input = Some(InputKind::Secondary);
1634 }
1635 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1636 next_input = Some(InputKind::Ability(0));
1637 } else if could_use_input(random_ability, ability_preferences) {
1638 next_input = Some(random_ability);
1639 } else if could_use_input(InputKind::Ability(4), ability_preferences)
1640 && rng.random_bool(2.0 * read_data.dt.0 as f64)
1641 {
1642 next_input = Some(InputKind::Ability(4));
1643 } else if rng.random_bool(0.5) {
1644 next_input = Some(InputKind::Primary);
1645 } else {
1646 next_input = Some(InputKind::Secondary);
1647 }
1648 };
1649 if let Some(input) = next_input {
1650 if could_use_input(input, ability_preferences) {
1651 controller.push_basic_input(input);
1652 false
1653 } else {
1654 true
1655 }
1656 } else {
1657 true
1658 }
1659 },
1660 SwordTactics::CripplingAdvanced => {
1661 let ability_preferences = AbilityPreferences {
1662 desired_energy: 50.0,
1663 combo_scaling_buildup: 0,
1664 };
1665 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1666 let mut next_input = None;
1667 if let Some(input) = current_input {
1668 continue_current_input(input, &mut next_input);
1669 } else {
1670 let stance_ability = InputKind::Ability(rng.random_range(1..3));
1671 let random_ability = InputKind::Ability(rng.random_range(1..5));
1672 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Crippling))) {
1673 if could_use_input(stance_ability, ability_preferences) {
1674 next_input = Some(stance_ability);
1675 } else if rng.random_bool(0.5) {
1676 next_input = Some(InputKind::Primary);
1677 } else {
1678 next_input = Some(InputKind::Secondary);
1679 }
1680 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1681 next_input = Some(InputKind::Ability(0));
1682 } else if could_use_input(random_ability, ability_preferences) {
1683 next_input = Some(random_ability);
1684 } else if rng.random_bool(0.5) {
1685 next_input = Some(InputKind::Primary);
1686 } else {
1687 next_input = Some(InputKind::Secondary);
1688 }
1689 };
1690 if let Some(input) = next_input {
1691 if could_use_input(input, ability_preferences) {
1692 controller.push_basic_input(input);
1693 false
1694 } else {
1695 true
1696 }
1697 } else {
1698 true
1699 }
1700 },
1701 SwordTactics::CleavingAdvanced => {
1702 let ability_preferences = AbilityPreferences {
1703 desired_energy: 50.0,
1704 combo_scaling_buildup: 0,
1705 };
1706 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1707 let mut next_input = None;
1708 if let Some(input) = current_input {
1709 continue_current_input(input, &mut next_input);
1710 } else {
1711 let stance_ability = InputKind::Ability(rng.random_range(1..3));
1712 let random_ability = InputKind::Ability(rng.random_range(1..5));
1713 if !matches!(self.stance, Some(Stance::Sword(SwordStance::Cleaving))) {
1714 if could_use_input(stance_ability, ability_preferences) {
1715 next_input = Some(stance_ability);
1716 } else if rng.random_bool(0.5) {
1717 next_input = Some(InputKind::Primary);
1718 } else {
1719 next_input = Some(InputKind::Secondary);
1720 }
1721 } else if could_use_input(InputKind::Ability(0), ability_preferences) {
1722 next_input = Some(InputKind::Ability(0));
1723 } else if could_use_input(random_ability, ability_preferences) {
1724 next_input = Some(random_ability);
1725 } else if rng.random_bool(0.5) {
1726 next_input = Some(InputKind::Primary);
1727 } else {
1728 next_input = Some(InputKind::Secondary);
1729 }
1730 };
1731 if let Some(input) = next_input {
1732 if could_use_input(input, ability_preferences) {
1733 controller.push_basic_input(input);
1734 false
1735 } else {
1736 true
1737 }
1738 } else {
1739 true
1740 }
1741 },
1742 }
1743 } else {
1744 false
1745 };
1746
1747 if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
1748 self.path_toward_target(
1749 agent,
1750 controller,
1751 tgt_data.pos.0,
1752 read_data,
1753 Path::Separate,
1754 None,
1755 );
1756 }
1757 }
1758
1759 pub fn handle_axe_attack(
1760 &self,
1761 agent: &mut Agent,
1762 controller: &mut Controller,
1763 attack_data: &AttackData,
1764 tgt_data: &TargetData,
1765 read_data: &ReadData,
1766 rng: &mut impl RngExt,
1767 ) {
1768 if !agent.combat_state.initialized {
1769 agent.combat_state.initialized = true;
1770 let available_tactics = {
1771 let mut tactics = Vec::new();
1772 let try_tactic = |skill, tactic, tactics: &mut Vec<AxeTactics>| {
1773 if self.skill_set.has_skill(Skill::Axe(skill)) {
1774 tactics.push(tactic);
1775 }
1776 };
1777 try_tactic(AxeSkill::Execute, AxeTactics::SavageAdvanced, &mut tactics);
1778 try_tactic(
1779 AxeSkill::Lacerate,
1780 AxeTactics::MercilessAdvanced,
1781 &mut tactics,
1782 );
1783 try_tactic(AxeSkill::Bulkhead, AxeTactics::RivingAdvanced, &mut tactics);
1784 if tactics.is_empty() {
1785 try_tactic(
1786 AxeSkill::RisingTide,
1787 AxeTactics::SavageIntermediate,
1788 &mut tactics,
1789 );
1790 try_tactic(
1791 AxeSkill::FierceRaze,
1792 AxeTactics::MercilessIntermediate,
1793 &mut tactics,
1794 );
1795 try_tactic(
1796 AxeSkill::Plunder,
1797 AxeTactics::RivingIntermediate,
1798 &mut tactics,
1799 );
1800 }
1801 if tactics.is_empty() {
1802 try_tactic(
1803 AxeSkill::BrutalSwing,
1804 AxeTactics::SavageSimple,
1805 &mut tactics,
1806 );
1807 try_tactic(AxeSkill::Rake, AxeTactics::MercilessSimple, &mut tactics);
1808 try_tactic(AxeSkill::SkullBash, AxeTactics::RivingSimple, &mut tactics);
1809 }
1810 if tactics.is_empty() {
1811 tactics.push(AxeTactics::Unskilled);
1812 }
1813 tactics
1814 };
1815
1816 let tactic = available_tactics
1817 .choose(rng)
1818 .copied()
1819 .unwrap_or(AxeTactics::Unskilled);
1820
1821 agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
1822
1823 let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
1824 let set_axe_ability = |controller: &mut Controller, slot, skill| {
1825 controller.push_event(ControlEvent::ChangeAbility {
1826 slot,
1827 auxiliary_key,
1828 new_ability: AuxiliaryAbility::MainWeapon(skill),
1829 });
1830 };
1831
1832 match tactic {
1833 AxeTactics::Unskilled => {},
1834 AxeTactics::SavageSimple => {
1835 set_axe_ability(controller, 0, 0);
1837 },
1838 AxeTactics::MercilessSimple => {
1839 set_axe_ability(controller, 0, 6);
1841 },
1842 AxeTactics::RivingSimple => {
1843 set_axe_ability(controller, 0, 12);
1845 },
1846 AxeTactics::SavageIntermediate => {
1847 set_axe_ability(controller, 0, 0);
1849 set_axe_ability(controller, 1, 1);
1851 set_axe_ability(controller, 2, 2);
1853 },
1854 AxeTactics::MercilessIntermediate => {
1855 set_axe_ability(controller, 0, 6);
1857 set_axe_ability(controller, 1, 7);
1859 set_axe_ability(controller, 2, 8);
1861 },
1862 AxeTactics::RivingIntermediate => {
1863 set_axe_ability(controller, 0, 12);
1865 set_axe_ability(controller, 1, 13);
1867 set_axe_ability(controller, 2, 14);
1869 },
1870 AxeTactics::SavageAdvanced => {
1871 set_axe_ability(controller, 0, 1);
1873 set_axe_ability(controller, 1, 2);
1875 set_axe_ability(controller, 2, 3);
1877 set_axe_ability(controller, 3, 4);
1879 set_axe_ability(controller, 4, 5);
1881 },
1882 AxeTactics::MercilessAdvanced => {
1883 set_axe_ability(controller, 0, 7);
1885 set_axe_ability(controller, 1, 8);
1887 set_axe_ability(controller, 2, 9);
1889 set_axe_ability(controller, 3, 10);
1891 set_axe_ability(controller, 4, 11);
1893 },
1894 AxeTactics::RivingAdvanced => {
1895 set_axe_ability(controller, 0, 13);
1897 set_axe_ability(controller, 1, 14);
1899 set_axe_ability(controller, 2, 15);
1901 set_axe_ability(controller, 3, 16);
1903 set_axe_ability(controller, 4, 17);
1905 },
1906 }
1907
1908 agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
1909 ActionMode::Reckless as u8;
1910 }
1911
1912 enum IntCounters {
1913 Tactic = 0,
1914 ActionMode = 1,
1915 }
1916
1917 enum Timers {
1918 GuardedCycle = 0,
1919 PosTimeOut = 1,
1920 }
1921
1922 enum Conditions {
1923 GuardedDefend = 0,
1924 RollingBreakThrough = 1,
1925 }
1926
1927 enum FloatCounters {
1928 GuardedTimer = 0,
1929 }
1930
1931 enum Positions {
1932 GuardedCover = 0,
1933 Flee = 1,
1934 }
1935
1936 let attempt_attack = handle_attack_aggression(
1937 self,
1938 agent,
1939 controller,
1940 attack_data,
1941 tgt_data,
1942 read_data,
1943 rng,
1944 Timers::PosTimeOut as usize,
1945 Timers::GuardedCycle as usize,
1946 FloatCounters::GuardedTimer as usize,
1947 IntCounters::ActionMode as usize,
1948 Conditions::GuardedDefend as usize,
1949 Conditions::RollingBreakThrough as usize,
1950 Positions::GuardedCover as usize,
1951 Positions::Flee as usize,
1952 );
1953
1954 let attack_failed = if attempt_attack {
1955 let primary = self.extract_ability(AbilityInput::Primary);
1956 let secondary = self.extract_ability(AbilityInput::Secondary);
1957 let abilities = [
1958 self.extract_ability(AbilityInput::Auxiliary(0)),
1959 self.extract_ability(AbilityInput::Auxiliary(1)),
1960 self.extract_ability(AbilityInput::Auxiliary(2)),
1961 self.extract_ability(AbilityInput::Auxiliary(3)),
1962 self.extract_ability(AbilityInput::Auxiliary(4)),
1963 ];
1964 let could_use_input = |input, ability_preferences| match input {
1965 InputKind::Primary => primary.as_ref().is_some_and(|p| {
1966 p.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
1967 }),
1968 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
1969 s.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
1970 }),
1971 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
1972 a.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
1973 }),
1974 _ => false,
1975 };
1976 let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
1977 if matches!(current_input, InputKind::Secondary) {
1978 let charging =
1979 matches!(self.char_state.stage_section(), Some(StageSection::Charge));
1980 let charged = self
1981 .char_state
1982 .durations()
1983 .and_then(|durs| durs.charge)
1984 .zip(self.char_state.timer())
1985 .is_some_and(|(dur, timer)| timer > dur);
1986 if !(charging && charged) {
1987 *next_input = Some(InputKind::Secondary);
1988 }
1989 } else {
1990 *next_input = Some(current_input);
1991 }
1992 };
1993 let current_input = self.char_state.ability_info().map(|ai| ai.input);
1994 let ability_preferences = AbilityPreferences {
1995 desired_energy: 40.0,
1996 combo_scaling_buildup: 15,
1997 };
1998 let mut next_input = None;
1999 if let Some(input) = current_input {
2000 continue_current_input(input, &mut next_input);
2001 } else {
2002 match AxeTactics::from_u8(
2003 agent.combat_state.int_counters[IntCounters::Tactic as usize],
2004 ) {
2005 AxeTactics::Unskilled => {
2006 if rng.random_bool(0.5) {
2007 next_input = Some(InputKind::Primary);
2008 } else {
2009 next_input = Some(InputKind::Secondary);
2010 }
2011 },
2012 AxeTactics::SavageSimple
2013 | AxeTactics::MercilessSimple
2014 | AxeTactics::RivingSimple => {
2015 if could_use_input(InputKind::Ability(0), ability_preferences) {
2016 next_input = Some(InputKind::Ability(0));
2017 } else if rng.random_bool(0.5) {
2018 next_input = Some(InputKind::Primary);
2019 } else {
2020 next_input = Some(InputKind::Secondary);
2021 }
2022 },
2023 AxeTactics::SavageIntermediate
2024 | AxeTactics::MercilessIntermediate
2025 | AxeTactics::RivingIntermediate => {
2026 let random_ability = InputKind::Ability(rng.random_range(0..3));
2027 if could_use_input(random_ability, ability_preferences) {
2028 next_input = Some(random_ability);
2029 } else if rng.random_bool(0.5) {
2030 next_input = Some(InputKind::Primary);
2031 } else {
2032 next_input = Some(InputKind::Secondary);
2033 }
2034 },
2035 AxeTactics::SavageAdvanced
2036 | AxeTactics::MercilessAdvanced
2037 | AxeTactics::RivingAdvanced => {
2038 let random_ability = InputKind::Ability(rng.random_range(0..5));
2039 if could_use_input(random_ability, ability_preferences) {
2040 next_input = Some(random_ability);
2041 } else if rng.random_bool(0.5) {
2042 next_input = Some(InputKind::Primary);
2043 } else {
2044 next_input = Some(InputKind::Secondary);
2045 }
2046 },
2047 }
2048 }
2049 if let Some(input) = next_input {
2050 if could_use_input(input, ability_preferences) {
2051 controller.push_basic_input(input);
2052 false
2053 } else {
2054 true
2055 }
2056 } else {
2057 true
2058 }
2059 } else {
2060 false
2061 };
2062
2063 if attack_failed && attack_data.dist_sqrd > 1.5_f32.powi(2) {
2064 self.path_toward_target(
2065 agent,
2066 controller,
2067 tgt_data.pos.0,
2068 read_data,
2069 Path::Separate,
2070 None,
2071 );
2072 }
2073 }
2074
2075 pub fn handle_bow_attack(
2076 &self,
2077 agent: &mut Agent,
2078 controller: &mut Controller,
2079 attack_data: &AttackData,
2080 tgt_data: &TargetData,
2081 read_data: &ReadData,
2082 rng: &mut impl RngExt,
2083 ) {
2084 if !agent.combat_state.initialized {
2085 agent.combat_state.initialized = true;
2086 let available_tactics = {
2087 let mut tactics = Vec::new();
2088 let try_tactic = |skill, tactic, tactics: &mut Vec<BowTactics>| {
2089 if self.skill_set.has_skill(Skill::Bow(skill)) {
2090 tactics.push(tactic);
2091 }
2092 };
2093 try_tactic(
2094 BowSkill::Heartseeker,
2095 BowTactics::HunterAdvanced,
2096 &mut tactics,
2097 );
2098 try_tactic(
2099 BowSkill::FreezeArrow,
2100 BowTactics::TricksterAdvanced,
2101 &mut tactics,
2102 );
2103 try_tactic(
2104 BowSkill::Fusillade,
2105 BowTactics::ArtilleryAdvanced,
2106 &mut tactics,
2107 );
2108 if tactics.is_empty() {
2109 try_tactic(
2110 BowSkill::StormChaser,
2111 BowTactics::HunterIntermediate,
2112 &mut tactics,
2113 );
2114 try_tactic(
2115 BowSkill::IgniteArrow,
2116 BowTactics::TricksterIntermediate,
2117 &mut tactics,
2118 );
2119 try_tactic(
2120 BowSkill::PiercingGale,
2121 BowTactics::ArtilleryIntermediate,
2122 &mut tactics,
2123 );
2124 }
2125 if tactics.is_empty() {
2126 try_tactic(BowSkill::ArdentHunt, BowTactics::HunterSimple, &mut tactics);
2127 try_tactic(
2128 BowSkill::SepticShot,
2129 BowTactics::TricksterSimple,
2130 &mut tactics,
2131 );
2132 try_tactic(BowSkill::Barrage, BowTactics::ArtillerySimple, &mut tactics);
2133 }
2134 if tactics.is_empty() {
2135 try_tactic(BowSkill::HeavyNock, BowTactics::Simple, &mut tactics);
2136 }
2137 if tactics.is_empty() {
2138 tactics.push(BowTactics::Unskilled);
2139 }
2140 tactics
2141 };
2142
2143 let tactic = available_tactics
2144 .choose(rng)
2145 .copied()
2146 .unwrap_or(BowTactics::Unskilled);
2147
2148 agent.combat_state.int_counters[IntCounters::Tactic as usize] = tactic as u8;
2149
2150 let auxiliary_key = ActiveAbilities::active_auxiliary_key(Some(self.inventory));
2151 let set_ability = |controller: &mut Controller, slot, skill| {
2152 controller.push_event(ControlEvent::ChangeAbility {
2153 slot,
2154 auxiliary_key,
2155 new_ability: AuxiliaryAbility::MainWeapon(skill),
2156 });
2157 };
2158 let mut set_random = |controller: &mut Controller, slot, options: &mut Vec<usize>| {
2159 if options.is_empty() {
2160 return;
2161 }
2162 let i = rng.random_range(0..options.len());
2163 set_ability(controller, slot, options.swap_remove(i));
2164 };
2165
2166 match tactic {
2167 BowTactics::Unskilled => {},
2168 BowTactics::Simple => {
2169 set_ability(controller, 0, rng.random_range(0..2));
2171 },
2172 BowTactics::HunterSimple => {
2173 set_ability(controller, 0, 0);
2175 set_ability(controller, 1, 1);
2177 set_ability(controller, 2, 2);
2179 },
2180 BowTactics::HunterIntermediate => {
2181 set_ability(controller, 0, 2);
2183 let mut options = vec![0, 1, 3, 4];
2185 set_random(controller, 1, &mut options);
2186 set_random(controller, 2, &mut options);
2187 set_random(controller, 3, &mut options);
2188 },
2189 BowTactics::HunterAdvanced => {
2190 let mut options = vec![2, 3, 4, 5, 6];
2192 set_random(controller, 1, &mut options);
2193 set_random(controller, 2, &mut options);
2194 set_random(controller, 3, &mut options);
2195 set_random(controller, 4, &mut options);
2196 set_ability(controller, 0, rng.random_range(0..2));
2198 },
2199 BowTactics::TricksterSimple => {
2200 set_ability(controller, 0, 0);
2202 set_ability(controller, 1, 1);
2204 set_ability(controller, 2, 7);
2206 },
2207 BowTactics::TricksterIntermediate => {
2208 set_ability(controller, 0, 7);
2210 let mut options = vec![0, 1, 8, 9];
2212 set_random(controller, 1, &mut options);
2213 set_random(controller, 2, &mut options);
2214 set_random(controller, 3, &mut options);
2215 },
2216 BowTactics::TricksterAdvanced => {
2217 let mut options = vec![7, 8, 9, 10, 11];
2219 set_random(controller, 1, &mut options);
2220 set_random(controller, 2, &mut options);
2221 set_random(controller, 3, &mut options);
2222 set_random(controller, 4, &mut options);
2223 set_ability(controller, 0, rng.random_range(0..2));
2225 },
2226 BowTactics::ArtillerySimple => {
2227 set_ability(controller, 0, 0);
2229 set_ability(controller, 1, 1);
2231 set_ability(controller, 2, 12);
2233 },
2234 BowTactics::ArtilleryIntermediate => {
2235 set_ability(controller, 0, 12);
2237 let mut options = vec![0, 1, 13, 14];
2239 set_random(controller, 1, &mut options);
2240 set_random(controller, 2, &mut options);
2241 set_random(controller, 3, &mut options);
2242 },
2243 BowTactics::ArtilleryAdvanced => {
2244 let mut options = vec![12, 13, 14, 15, 16];
2246 set_random(controller, 1, &mut options);
2247 set_random(controller, 2, &mut options);
2248 set_random(controller, 3, &mut options);
2249 set_random(controller, 4, &mut options);
2250 set_ability(controller, 0, rng.random_range(0..2));
2252 },
2253 }
2254 }
2255
2256 enum IntCounters {
2257 Tactic = 0,
2258 ActionMode = 1,
2259 }
2260
2261 enum Conditions {
2262 RollingBreakThrough = 0,
2263 MaintainDist = 1,
2264 CreateDist = 2,
2265 }
2266
2267 enum Positions {
2268 Flee = 0,
2269 Maintain = 1,
2270 }
2271
2272 enum Timers {
2273 GuardedCycle = 0,
2274 }
2275
2276 enum FloatCounters {
2277 GuardedCycle = 0,
2278 }
2279
2280 let attempt_attack = {
2284 if let Some(health) = self.health {
2285 agent.combat_state.int_counters[IntCounters::ActionMode as usize] =
2286 if health.fraction() < 0.2 {
2287 ActionMode::Fleeing as u8
2288 } else if health.fraction() < 0.95 {
2289 ActionMode::Guarded as u8
2290 } else {
2291 ActionMode::Reckless as u8
2292 };
2293 }
2294
2295 let range1 = rng.random_range(6.0..10.0);
2296 let range2 = rng.random_range(8.0..15.0);
2297
2298 let mut flee_handler = |agent: &mut Agent| {
2299 if agent.combat_state.conditions[Conditions::RollingBreakThrough as usize] {
2300 controller.push_basic_input(InputKind::Roll);
2301 agent.combat_state.conditions[Conditions::RollingBreakThrough as usize] = false;
2302 }
2303 if let Some(pos) = agent.combat_state.positions[Positions::Flee as usize] {
2304 if let Some(dir) = Dir::from_unnormalized(pos - self.pos.0) {
2305 controller.inputs.look_dir = dir;
2306 }
2307 if pos.distance_squared(self.pos.0) < 5_f32.powi(2) {
2308 agent.combat_state.positions[Positions::Flee as usize] = None;
2309 }
2310 self.path_toward_target(
2311 agent,
2312 controller,
2313 pos,
2314 read_data,
2315 Path::Separate,
2316 None,
2317 );
2318 } else {
2319 agent.combat_state.positions[Positions::Flee as usize] = {
2320 let rand_dir = {
2321 let dir = (self.pos.0 - tgt_data.pos.0)
2322 .try_normalized()
2323 .unwrap_or(Vec3::unit_x())
2324 .xy();
2325 dir.rotated_z(rng.random_range(-0.75..0.75))
2326 };
2327 let attempted_dist = rng.random_range(16.0..26.0);
2328 let actual_dist = read_data
2329 .terrain
2330 .ray(
2331 self.pos.0 + Vec3::unit_z() * 0.5,
2332 self.pos.0 + Vec3::unit_z() * 0.5 + rand_dir * attempted_dist,
2333 )
2334 .until(Block::is_solid)
2335 .cast()
2336 .0
2337 - 1.0;
2338 if actual_dist < 10.0 {
2339 let dist = read_data
2340 .terrain
2341 .ray(
2342 self.pos.0 + Vec3::unit_z() * 0.5,
2343 self.pos.0 + Vec3::unit_z() * 0.5 - rand_dir * attempted_dist,
2344 )
2345 .until(Block::is_solid)
2346 .cast()
2347 .0
2348 - 1.0;
2349 agent.combat_state.conditions
2350 [Conditions::RollingBreakThrough as usize] = true;
2351 Some(self.pos.0 - rand_dir * dist)
2352 } else {
2353 Some(self.pos.0 + rand_dir * actual_dist)
2354 }
2355 };
2356 }
2357 };
2358
2359 match ActionMode::from_u8(
2360 agent.combat_state.int_counters[IntCounters::ActionMode as usize],
2361 ) {
2362 ActionMode::Reckless => true,
2363 ActionMode::Guarded => {
2364 agent.combat_state.timers[Timers::GuardedCycle as usize] += read_data.dt.0;
2365 if agent.combat_state.timers[Timers::GuardedCycle as usize]
2366 > agent.combat_state.counters[FloatCounters::GuardedCycle as usize]
2367 {
2368 agent.combat_state.timers[Timers::GuardedCycle as usize] = 0.0;
2369 agent.combat_state.conditions[Conditions::MaintainDist as usize] ^= true;
2370 agent.combat_state.counters[FloatCounters::GuardedCycle as usize] =
2371 if agent.combat_state.conditions[Conditions::MaintainDist as usize] {
2372 range1
2373 } else {
2374 range2
2375 };
2376 }
2377 if let Some(pos) = agent.combat_state.positions[Positions::Maintain as usize]
2378 && pos.distance_squared(self.pos.0) < 5_f32.powi(2)
2379 {
2380 agent.combat_state.positions[Positions::Maintain as usize] = None;
2381 }
2382 let circle = if agent.combat_state.conditions[Conditions::MaintainDist as usize]
2383 {
2384 if attack_data.dist_sqrd < 7_f32.powi(2) {
2385 agent.combat_state.conditions[Conditions::CreateDist as usize] = true;
2386 }
2387 if attack_data.dist_sqrd > 12_f32.powi(2) {
2388 agent.combat_state.conditions[Conditions::CreateDist as usize] = false;
2389 }
2390 if agent.combat_state.conditions[Conditions::CreateDist as usize] {
2391 flee_handler(agent);
2392 false
2393 } else {
2394 true
2395 }
2396 } else {
2397 true
2398 };
2399 if circle {
2400 if let Some(pos) =
2401 agent.combat_state.positions[Positions::Maintain as usize]
2402 {
2403 self.path_toward_target(
2404 agent,
2405 controller,
2406 pos,
2407 read_data,
2408 Path::Separate,
2409 None,
2410 );
2411 } else {
2412 agent.combat_state.positions[Positions::Maintain as usize] = {
2413 let rand_dir = {
2414 let dir = (tgt_data.pos.0 - self.pos.0)
2415 .try_normalized()
2416 .unwrap_or(Vec3::unit_x())
2417 .xy();
2418 if rng.random_bool(0.5) {
2419 dir.rotated_z(PI / 2.0 + rng.random_range(0.0..0.75))
2420 } else {
2421 dir.rotated_z(-PI / 2.0 - rng.random_range(0.0..0.75))
2422 }
2423 };
2424 let attempted_dist = rng.random_range(12.0..20.0);
2425 let actual_dist = read_data
2426 .terrain
2427 .ray(
2428 self.pos.0 + Vec3::unit_z() * 0.5,
2429 self.pos.0
2430 + Vec3::unit_z() * 0.5
2431 + rand_dir * attempted_dist,
2432 )
2433 .until(Block::is_solid)
2434 .cast()
2435 .0
2436 - 1.0;
2437 Some(self.pos.0 + rand_dir * actual_dist)
2438 };
2439 }
2440 true
2441 } else {
2442 false
2443 }
2444 },
2445 ActionMode::Fleeing => {
2446 flee_handler(agent);
2447 false
2448 },
2449 }
2450 };
2451
2452 let attack_failed = if attempt_attack {
2453 let primary = self.extract_ability(AbilityInput::Primary);
2454 let secondary = self.extract_ability(AbilityInput::Secondary);
2455 let abilities = [
2456 self.extract_ability(AbilityInput::Auxiliary(0)),
2457 self.extract_ability(AbilityInput::Auxiliary(1)),
2458 self.extract_ability(AbilityInput::Auxiliary(2)),
2459 self.extract_ability(AbilityInput::Auxiliary(3)),
2460 self.extract_ability(AbilityInput::Auxiliary(4)),
2461 ];
2462 let could_use_input = |input, ability_preferences| match input {
2463 InputKind::Primary => primary.as_ref().is_some_and(|p| {
2464 p.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
2465 }),
2466 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
2467 s.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
2468 }),
2469 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
2470 let ability = self.active_abilities.get_ability(
2471 AbilityInput::Auxiliary(x),
2472 Some(self.inventory),
2473 Some(self.skill_set),
2474 self.stats,
2475 );
2476 let additional_conditions = match ability {
2477 Ability::MainWeaponAux(8) => self
2478 .inventory
2479 .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2480 &AbilityReqItem::Firedrop,
2481 ))
2482 .is_some(),
2483 Ability::MainWeaponAux(9) => self
2484 .inventory
2485 .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2486 &AbilityReqItem::PoisonClot,
2487 ))
2488 .is_some(),
2489 Ability::MainWeaponAux(10) => self
2490 .inventory
2491 .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2492 &AbilityReqItem::GelidGel,
2493 ))
2494 .is_some(),
2495 Ability::MainWeaponAux(11) => self
2496 .inventory
2497 .get_slot_of_item_by_def_id(&AbilityReqItem::item_def_id(
2498 &AbilityReqItem::LevinDust,
2499 ))
2500 .is_some(),
2501 _ => true,
2502 };
2503 a.could_use(attack_data, self, tgt_data, read_data, ability_preferences)
2504 && additional_conditions
2505 }),
2506 _ => false,
2507 };
2508 let continue_current_input = |current_input, next_input: &mut Option<InputKind>| {
2509 let charging =
2510 matches!(self.char_state.stage_section(), Some(StageSection::Charge));
2511 let charged = self
2512 .char_state
2513 .durations()
2514 .and_then(|durs| durs.charge)
2515 .zip(self.char_state.timer())
2516 .is_some_and(|(dur, timer)| timer > dur);
2517 let recover =
2518 matches!(self.char_state.stage_section(), Some(StageSection::Recover));
2519
2520 if !(recover || (charging && charged)) {
2521 *next_input = Some(current_input);
2522 }
2523 };
2524 let prefer_m2 = matches!(
2525 self.stance,
2526 Some(Stance::Bow(
2527 BowStance::Barrage | BowStance::Hawkstrike | BowStance::Heartseeker
2528 ))
2529 );
2530 let current_input = self.char_state.ability_info().map(|ai| ai.input);
2531 let ability_preferences = AbilityPreferences {
2532 desired_energy: 40.0,
2533 combo_scaling_buildup: 0,
2534 };
2535 let mut next_input = None;
2536 if let Some(input) = current_input {
2537 continue_current_input(input, &mut next_input);
2538 } else if prefer_m2 {
2539 if could_use_input(InputKind::Secondary, ability_preferences) {
2540 next_input = Some(InputKind::Secondary);
2541 } else {
2542 next_input = Some(InputKind::Primary);
2543 }
2544 } else {
2545 match BowTactics::from_u8(
2546 agent.combat_state.int_counters[IntCounters::Tactic as usize],
2547 ) {
2548 BowTactics::Unskilled => {
2549 if rng.random_bool(0.5) {
2550 next_input = Some(InputKind::Primary);
2551 } else {
2552 next_input = Some(InputKind::Secondary);
2553 }
2554 },
2555 BowTactics::Simple => {
2556 if could_use_input(InputKind::Ability(0), ability_preferences) {
2557 next_input = Some(InputKind::Ability(0));
2558 } else if rng.random_bool(0.5) {
2559 next_input = Some(InputKind::Primary);
2560 } else {
2561 next_input = Some(InputKind::Secondary);
2562 }
2563 },
2564 BowTactics::HunterSimple
2565 | BowTactics::TricksterSimple
2566 | BowTactics::ArtillerySimple => {
2567 let random_ability = InputKind::Ability(rng.random_range(0..3));
2568 if could_use_input(random_ability, ability_preferences) {
2569 next_input = Some(random_ability);
2570 } else if rng.random_bool(0.5) {
2571 next_input = Some(InputKind::Primary);
2572 } else {
2573 next_input = Some(InputKind::Secondary);
2574 }
2575 },
2576 BowTactics::HunterIntermediate
2577 | BowTactics::TricksterIntermediate
2578 | BowTactics::ArtilleryIntermediate => {
2579 let random_ability = InputKind::Ability(rng.random_range(0..4));
2580 if could_use_input(random_ability, ability_preferences) {
2581 next_input = Some(random_ability);
2582 } else if rng.random_bool(0.5) {
2583 next_input = Some(InputKind::Primary);
2584 } else {
2585 next_input = Some(InputKind::Secondary);
2586 }
2587 },
2588 BowTactics::HunterAdvanced
2589 | BowTactics::TricksterAdvanced
2590 | BowTactics::ArtilleryAdvanced => {
2591 let random_ability = InputKind::Ability(rng.random_range(0..5));
2592 if could_use_input(random_ability, ability_preferences) {
2593 next_input = Some(random_ability);
2594 } else if rng.random_bool(0.5) {
2595 next_input = Some(InputKind::Primary);
2596 } else {
2597 next_input = Some(InputKind::Secondary);
2598 }
2599 },
2600 }
2601 }
2602 if let Some(input) = next_input {
2603 if could_use_input(input, ability_preferences) {
2604 let is_death_volley = if let Some(ability_input) = input.into() {
2605 let raw_input = self.active_abilities.get_ability(
2606 ability_input,
2607 Some(self.inventory),
2608 Some(self.skill_set),
2609 self.stats,
2610 );
2611 raw_input == Ability::MainWeaponAux(16)
2612 } else {
2613 false
2614 };
2615 if is_death_volley {
2616 controller.push_action(ControlAction::StartInput {
2617 input,
2618 target_entity: None,
2619 select_pos: Some(tgt_data.pos.0),
2620 });
2621 } else {
2622 controller.push_basic_input(input);
2623 }
2624 false
2625 } else {
2626 true
2627 }
2628 } else {
2629 true
2630 }
2631 } else {
2632 false
2633 };
2634
2635 if attack_failed
2636 && (attack_data.dist_sqrd > 25_f32.powi(2)
2637 || !entities_have_line_of_sight(
2638 self.pos,
2639 self.body,
2640 self.scale,
2641 tgt_data.pos,
2642 tgt_data.body,
2643 tgt_data.scale,
2644 read_data,
2645 ))
2646 {
2647 self.path_toward_target(
2648 agent,
2649 controller,
2650 tgt_data.pos.0,
2651 read_data,
2652 Path::Separate,
2653 None,
2654 );
2655 }
2656 }
2657
2658 pub fn handle_staff_attack(
2659 &self,
2660 agent: &mut Agent,
2661 controller: &mut Controller,
2662 attack_data: &AttackData,
2663 tgt_data: &TargetData,
2664 read_data: &ReadData,
2665 rng: &mut impl RngExt,
2666 ) {
2667 enum ActionStateConditions {
2668 ConditionStaffCanShockwave = 0,
2669 }
2670 let extract_ability = |input: AbilityInput| {
2671 self.active_abilities
2672 .activate_ability(
2673 input,
2674 Some(self.inventory),
2675 self.skill_set,
2676 self.body,
2677 Some(self.char_state),
2678 self.stance,
2679 self.combo,
2680 self.stats,
2681 self.buffs,
2682 )
2683 .map_or(Default::default(), |a| a.0)
2684 };
2685 let (flamethrower, shockwave) = (
2686 extract_ability(AbilityInput::Secondary),
2687 extract_ability(AbilityInput::Auxiliary(0)),
2688 );
2689 let flamethrower_range = match flamethrower {
2690 CharacterAbility::BasicBeam { range, .. } => range,
2691 _ => 20.0_f32,
2692 };
2693 let shockwave_cost = shockwave.energy_cost();
2694 if self.body.is_some_and(|b| b.is_humanoid())
2695 && attack_data.in_min_range()
2696 && self.energy.current()
2697 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2698 && !matches!(self.char_state, CharacterState::Shockwave(_))
2699 {
2700 controller.push_basic_input(InputKind::Roll);
2703 } else if matches!(self.char_state, CharacterState::Shockwave(_)) {
2704 agent.combat_state.conditions
2705 [ActionStateConditions::ConditionStaffCanShockwave as usize] = false;
2706 } else if agent.combat_state.conditions
2707 [ActionStateConditions::ConditionStaffCanShockwave as usize]
2708 && matches!(self.char_state, CharacterState::Wielding(_))
2709 {
2710 controller.push_basic_input(InputKind::Ability(0));
2711 } else if !matches!(self.char_state, CharacterState::Shockwave(c) if !matches!(c.stage_section, StageSection::Recover))
2712 {
2713 let target_approaching_speed = -agent
2715 .target
2716 .as_ref()
2717 .map(|t| t.target)
2718 .and_then(|e| read_data.velocities.get(e))
2719 .map_or(0.0, |v| v.0.dot(self.ori.look_vec()));
2720 if self
2721 .skill_set
2722 .has_skill(Skill::Staff(StaffSkill::FireShockwave))
2723 && target_approaching_speed > 12.0
2724 && self.energy.current() > shockwave_cost
2725 {
2726 if matches!(self.char_state, CharacterState::Wielding(_)) {
2728 controller.push_basic_input(InputKind::Ability(0));
2729 } else {
2730 agent.combat_state.conditions
2731 [ActionStateConditions::ConditionStaffCanShockwave as usize] = true;
2732 }
2733 } else if self.energy.current()
2734 > shockwave_cost
2735 + CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2736 && attack_data.dist_sqrd < flamethrower_range.powi(2)
2737 {
2738 controller.push_basic_input(InputKind::Secondary);
2739 } else {
2740 controller.push_basic_input(InputKind::Primary);
2741 }
2742 }
2743 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2746 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2748 &*read_data.terrain,
2749 self.pos.0,
2750 self.vel.0,
2751 tgt_data.pos.0,
2752 TraversalConfig {
2753 min_tgt_dist: 1.25,
2754 ..self.traversal_config
2755 },
2756 &read_data.time,
2757 ) {
2758 self.unstuck_if(stuck, controller);
2759 controller.inputs.move_dir =
2760 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2761 }
2762 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2763 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2765 &*read_data.terrain,
2766 self.pos.0,
2767 self.vel.0,
2768 tgt_data.pos.0,
2769 TraversalConfig {
2770 min_tgt_dist: 1.25,
2771 ..self.traversal_config
2772 },
2773 &read_data.time,
2774 ) {
2775 self.unstuck_if(stuck, controller);
2776 if entities_have_line_of_sight(
2777 self.pos,
2778 self.body,
2779 self.scale,
2780 tgt_data.pos,
2781 tgt_data.body,
2782 tgt_data.scale,
2783 read_data,
2784 ) && attack_data.angle < 45.0
2785 {
2786 controller.inputs.move_dir = bearing
2787 .xy()
2788 .rotated_z(rng.random_range(-1.57..-0.5))
2789 .try_normalized()
2790 .unwrap_or_else(Vec2::zero)
2791 * speed;
2792 } else {
2793 controller.inputs.move_dir =
2795 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2796 self.jump_if(bearing.z > 1.5, controller);
2797 controller.inputs.move_z = bearing.z;
2798 }
2799 }
2800 if self.body.is_some_and(|b| b.is_humanoid())
2802 && attack_data.dist_sqrd < 16.0f32.powi(2)
2803 && !matches!(self.char_state, CharacterState::Shockwave(_))
2804 && rng.random::<f32>() < 0.02
2805 {
2806 controller.push_basic_input(InputKind::Roll);
2807 }
2808 } else {
2809 self.path_toward_target(
2811 agent,
2812 controller,
2813 tgt_data.pos.0,
2814 read_data,
2815 Path::AtTarget,
2816 None,
2817 );
2818 }
2819 }
2820
2821 pub fn handle_sceptre_attack(
2822 &self,
2823 agent: &mut Agent,
2824 controller: &mut Controller,
2825 attack_data: &AttackData,
2826 tgt_data: &TargetData,
2827 read_data: &ReadData,
2828 rng: &mut impl RngExt,
2829 ) {
2830 const DESIRED_ENERGY_LEVEL: f32 = 50.0;
2831 const DESIRED_COMBO_LEVEL: u32 = 8;
2832
2833 let line_of_sight_with_target = || {
2834 entities_have_line_of_sight(
2835 self.pos,
2836 self.body,
2837 self.scale,
2838 tgt_data.pos,
2839 tgt_data.body,
2840 tgt_data.scale,
2841 read_data,
2842 )
2843 };
2844
2845 if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
2847 && line_of_sight_with_target()
2848 {
2849 if self.energy.current() > DESIRED_ENERGY_LEVEL
2852 && read_data
2853 .combos
2854 .get(*self.entity)
2855 .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
2856 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
2857 buff.iter_kind(BuffKind::Regeneration)
2858 .peekable()
2859 .peek()
2860 .is_some()
2861 })
2862 {
2863 controller.push_basic_input(InputKind::Secondary);
2865 } else if self
2866 .skill_set
2867 .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
2868 && self.energy.current() > DESIRED_ENERGY_LEVEL
2869 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
2870 buff.iter_kind(BuffKind::ProtectingWard)
2871 .peekable()
2872 .peek()
2873 .is_some()
2874 })
2875 {
2876 controller.push_basic_input(InputKind::Ability(0));
2879 } else {
2880 controller.push_basic_input(InputKind::Primary);
2883 }
2884 } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2885 if self.body.is_some_and(|b| b.is_humanoid())
2886 && self.energy.current()
2887 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
2888 && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
2889 {
2890 controller.push_basic_input(InputKind::Roll);
2893 } else if attack_data.angle < 15.0 {
2894 controller.push_basic_input(InputKind::Primary);
2895 }
2896 }
2897 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
2900 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2902 &*read_data.terrain,
2903 self.pos.0,
2904 self.vel.0,
2905 tgt_data.pos.0,
2906 TraversalConfig {
2907 min_tgt_dist: 1.25,
2908 ..self.traversal_config
2909 },
2910 &read_data.time,
2911 ) {
2912 self.unstuck_if(stuck, controller);
2913 controller.inputs.move_dir =
2914 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2915 }
2916 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2917 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
2919 &*read_data.terrain,
2920 self.pos.0,
2921 self.vel.0,
2922 tgt_data.pos.0,
2923 TraversalConfig {
2924 min_tgt_dist: 1.25,
2925 ..self.traversal_config
2926 },
2927 &read_data.time,
2928 ) {
2929 self.unstuck_if(stuck, controller);
2930 if line_of_sight_with_target() && attack_data.angle < 45.0 {
2931 controller.inputs.move_dir = bearing
2932 .xy()
2933 .rotated_z(rng.random_range(0.5..1.57))
2934 .try_normalized()
2935 .unwrap_or_else(Vec2::zero)
2936 * speed;
2937 } else {
2938 controller.inputs.move_dir =
2940 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
2941 self.jump_if(bearing.z > 1.5, controller);
2942 controller.inputs.move_z = bearing.z;
2943 }
2944 }
2945 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
2947 && !matches!(self.char_state, CharacterState::BasicAura(_))
2948 && attack_data.dist_sqrd < 16.0f32.powi(2)
2949 && rng.random::<f32>() < 0.01
2950 {
2951 controller.push_basic_input(InputKind::Roll);
2952 }
2953 } else {
2954 self.path_toward_target(
2956 agent,
2957 controller,
2958 tgt_data.pos.0,
2959 read_data,
2960 Path::AtTarget,
2961 None,
2962 );
2963 }
2964 }
2965
2966 pub fn handle_stone_golem_attack(
2967 &self,
2968 agent: &mut Agent,
2969 controller: &mut Controller,
2970 attack_data: &AttackData,
2971 tgt_data: &TargetData,
2972 read_data: &ReadData,
2973 ) {
2974 enum ActionStateTimers {
2975 TimerHandleStoneGolemAttack = 0, }
2977
2978 if attack_data.in_min_range() && attack_data.angle < 90.0 {
2979 controller.inputs.move_dir = Vec2::zero();
2980 controller.push_basic_input(InputKind::Primary);
2981 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
2983 if self.vel.0.is_approx_zero() {
2984 controller.push_basic_input(InputKind::Ability(0));
2985 }
2986 if self
2987 .path_toward_target(
2988 agent,
2989 controller,
2990 tgt_data.pos.0,
2991 read_data,
2992 Path::Separate,
2993 None,
2994 )
2995 .is_some()
2996 && entities_have_line_of_sight(
2997 self.pos,
2998 self.body,
2999 self.scale,
3000 tgt_data.pos,
3001 tgt_data.body,
3002 tgt_data.scale,
3003 read_data,
3004 )
3005 && attack_data.angle < 90.0
3006 {
3007 if agent.combat_state.timers
3008 [ActionStateTimers::TimerHandleStoneGolemAttack as usize]
3009 > 5.0
3010 {
3011 controller.push_basic_input(InputKind::Secondary);
3012 agent.combat_state.timers
3013 [ActionStateTimers::TimerHandleStoneGolemAttack as usize] = 0.0;
3014 } else {
3015 agent.combat_state.timers
3016 [ActionStateTimers::TimerHandleStoneGolemAttack as usize] += read_data.dt.0;
3017 }
3018 }
3019 } else {
3020 self.path_toward_target(
3021 agent,
3022 controller,
3023 tgt_data.pos.0,
3024 read_data,
3025 Path::AtTarget,
3026 None,
3027 );
3028 }
3029 }
3030
3031 pub fn handle_iron_golem_attack(
3032 &self,
3033 agent: &mut Agent,
3034 controller: &mut Controller,
3035 attack_data: &AttackData,
3036 tgt_data: &TargetData,
3037 read_data: &ReadData,
3038 ) {
3039 enum ActionStateTimers {
3040 AttackTimer = 0,
3041 }
3042
3043 let home = agent.patrol_origin.unwrap_or(self.pos.0);
3044
3045 let attack_select =
3046 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 3.0 {
3047 0
3048 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.5 {
3049 1
3050 } else {
3051 2
3052 };
3053 if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
3055 self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
3056 } else if tgt_data.pos.0.z > home.z + 5.0 {
3058 controller.push_basic_input(InputKind::Ability(0))
3059 } else if attack_data.in_min_range() {
3060 controller.inputs.move_dir = Vec2::zero();
3061 controller.push_basic_input(InputKind::Primary);
3062 } else {
3063 match attack_select {
3064 0 => {
3065 controller.push_basic_input(InputKind::Ability(0))
3067 },
3068 1 => {
3069 controller.push_basic_input(InputKind::Ability(1))
3071 },
3072 _ => {
3073 controller.push_basic_input(InputKind::Secondary)
3075 },
3076 };
3077 };
3078 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
3079 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 7.5 {
3080 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
3081 };
3082 }
3083
3084 pub fn handle_circle_charge_attack(
3085 &self,
3086 agent: &mut Agent,
3087 controller: &mut Controller,
3088 attack_data: &AttackData,
3089 tgt_data: &TargetData,
3090 read_data: &ReadData,
3091 radius: u32,
3092 circle_time: u32,
3093 rng: &mut impl RngExt,
3094 ) {
3095 enum ActionStateCountersF {
3096 CounterFHandleCircleChargeAttack = 0,
3097 }
3098
3099 enum ActionStateCountersI {
3100 CounterIHandleCircleChargeAttack = 0,
3101 }
3102
3103 if agent.combat_state.counters
3104 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
3105 >= circle_time as f32
3106 {
3107 controller.push_basic_input(InputKind::Secondary);
3109 }
3110 if attack_data.in_min_range() {
3111 if agent.combat_state.counters
3112 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
3113 > 0.0
3114 {
3115 agent.combat_state.counters
3117 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] = 0.0;
3118 agent.combat_state.int_counters
3119 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] = 0;
3120 } else {
3121 controller.push_basic_input(InputKind::Primary);
3123 controller.inputs.move_dir = Vec2::zero();
3124 }
3125 } else if attack_data.dist_sqrd < (radius as f32 + attack_data.min_attack_dist).powi(2) {
3126 if agent.combat_state.int_counters
3128 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize]
3129 == 0
3130 {
3131 agent.combat_state.int_counters
3133 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize] =
3134 1 + rng.random_bool(0.5) as u8;
3135 }
3136 if agent.combat_state.counters
3137 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize]
3138 < circle_time as f32
3139 {
3140 let move_dir = match agent.combat_state.int_counters
3142 [ActionStateCountersI::CounterIHandleCircleChargeAttack as usize]
3143 {
3144 1 =>
3145 {
3147 (tgt_data.pos.0 - self.pos.0)
3148 .xy()
3149 .rotated_z(0.47 * PI)
3150 .try_normalized()
3151 .unwrap_or_else(Vec2::unit_y)
3152 },
3153 2 =>
3154 {
3156 (tgt_data.pos.0 - self.pos.0)
3157 .xy()
3158 .rotated_z(-0.47 * PI)
3159 .try_normalized()
3160 .unwrap_or_else(Vec2::unit_y)
3161 },
3162 _ =>
3163 {
3165 Vec2::zero()
3166 },
3167 };
3168 let obstacle = read_data
3169 .terrain
3170 .ray(
3171 self.pos.0 + Vec3::unit_z(),
3172 self.pos.0 + move_dir.with_z(0.0) * 2.0 + Vec3::unit_z(),
3173 )
3174 .until(Block::is_solid)
3175 .cast()
3176 .1
3177 .map_or(true, |b| b.is_some());
3178 if obstacle {
3179 agent.combat_state.counters
3181 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] =
3182 circle_time as f32;
3183 }
3184 controller.inputs.move_dir = move_dir;
3185 agent.combat_state.counters
3187 [ActionStateCountersF::CounterFHandleCircleChargeAttack as usize] +=
3188 read_data.dt.0;
3189 }
3190 } else {
3192 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3193 Path::Separate
3195 } else {
3196 Path::AtTarget
3197 };
3198 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
3199 }
3200 }
3201
3202 pub fn handle_quadlow_ranged_attack(
3203 &self,
3204 agent: &mut Agent,
3205 controller: &mut Controller,
3206 attack_data: &AttackData,
3207 tgt_data: &TargetData,
3208 read_data: &ReadData,
3209 ) {
3210 enum ActionStateTimers {
3211 TimerHandleQuadLowRanged = 0,
3212 }
3213
3214 if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2)
3215 && attack_data.angle < 90.0
3216 {
3217 controller.inputs.move_dir = if !attack_data.in_min_range() {
3218 (tgt_data.pos.0 - self.pos.0)
3219 .xy()
3220 .try_normalized()
3221 .unwrap_or_else(Vec2::unit_y)
3222 } else {
3223 Vec2::zero()
3224 };
3225
3226 controller.push_basic_input(InputKind::Primary);
3227 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3228 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
3229 &*read_data.terrain,
3230 self.pos.0,
3231 self.vel.0,
3232 tgt_data.pos.0,
3233 TraversalConfig {
3234 min_tgt_dist: 1.25,
3235 ..self.traversal_config
3236 },
3237 &read_data.time,
3238 ) {
3239 self.unstuck_if(stuck, controller);
3240 if attack_data.angle < 15.0
3241 && entities_have_line_of_sight(
3242 self.pos,
3243 self.body,
3244 self.scale,
3245 tgt_data.pos,
3246 tgt_data.body,
3247 tgt_data.scale,
3248 read_data,
3249 )
3250 {
3251 if agent.combat_state.timers
3252 [ActionStateTimers::TimerHandleQuadLowRanged as usize]
3253 > 5.0
3254 {
3255 agent.combat_state.timers
3256 [ActionStateTimers::TimerHandleQuadLowRanged as usize] = 0.0;
3257 } else if agent.combat_state.timers
3258 [ActionStateTimers::TimerHandleQuadLowRanged as usize]
3259 > 2.5
3260 {
3261 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3262 .xy()
3263 .rotated_z(1.75 * PI)
3264 .try_normalized()
3265 .unwrap_or_else(Vec2::zero)
3266 * speed;
3267 agent.combat_state.timers
3268 [ActionStateTimers::TimerHandleQuadLowRanged as usize] +=
3269 read_data.dt.0;
3270 } else {
3271 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3272 .xy()
3273 .rotated_z(0.25 * PI)
3274 .try_normalized()
3275 .unwrap_or_else(Vec2::zero)
3276 * speed;
3277 agent.combat_state.timers
3278 [ActionStateTimers::TimerHandleQuadLowRanged as usize] +=
3279 read_data.dt.0;
3280 }
3281 controller.push_basic_input(InputKind::Secondary);
3282 self.jump_if(bearing.z > 1.5, controller);
3283 controller.inputs.move_z = bearing.z;
3284 } else {
3285 controller.inputs.move_dir =
3286 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
3287 self.jump_if(bearing.z > 1.5, controller);
3288 controller.inputs.move_z = bearing.z;
3289 }
3290 } else {
3291 agent.target = None;
3292 }
3293 } else {
3294 self.path_toward_target(
3295 agent,
3296 controller,
3297 tgt_data.pos.0,
3298 read_data,
3299 Path::AtTarget,
3300 None,
3301 );
3302 }
3303 }
3304
3305 pub fn handle_tail_slap_attack(
3306 &self,
3307 agent: &mut Agent,
3308 controller: &mut Controller,
3309 attack_data: &AttackData,
3310 tgt_data: &TargetData,
3311 read_data: &ReadData,
3312 ) {
3313 enum ActionStateTimers {
3314 TimerTailSlap = 0,
3315 }
3316
3317 if attack_data.angle < 90.0
3318 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
3319 {
3320 if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 4.0 {
3321 controller.push_cancel_input(InputKind::Primary);
3322 agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] = 0.0;
3323 } else if agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] > 1.0 {
3324 controller.push_basic_input(InputKind::Primary);
3325 agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] +=
3326 read_data.dt.0;
3327 } else {
3328 controller.push_basic_input(InputKind::Secondary);
3329 agent.combat_state.timers[ActionStateTimers::TimerTailSlap as usize] +=
3330 read_data.dt.0;
3331 }
3332 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3333 .xy()
3334 .try_normalized()
3335 .unwrap_or_else(Vec2::unit_y)
3336 * 0.1;
3337 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3338 self.path_toward_target(
3339 agent,
3340 controller,
3341 tgt_data.pos.0,
3342 read_data,
3343 Path::Separate,
3344 None,
3345 );
3346 } else {
3347 self.path_toward_target(
3348 agent,
3349 controller,
3350 tgt_data.pos.0,
3351 read_data,
3352 Path::AtTarget,
3353 None,
3354 );
3355 }
3356 }
3357
3358 pub fn handle_quadlow_quick_attack(
3359 &self,
3360 agent: &mut Agent,
3361 controller: &mut Controller,
3362 attack_data: &AttackData,
3363 tgt_data: &TargetData,
3364 read_data: &ReadData,
3365 ) {
3366 if attack_data.angle < 90.0
3367 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
3368 {
3369 controller.inputs.move_dir = Vec2::zero();
3370 controller.push_basic_input(InputKind::Secondary);
3371 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2)
3372 && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2)
3373 && attack_data.angle < 90.0
3374 {
3375 controller.push_basic_input(InputKind::Primary);
3376 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3377 .xy()
3378 .rotated_z(-0.47 * PI)
3379 .try_normalized()
3380 .unwrap_or_else(Vec2::unit_y);
3381 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3382 self.path_toward_target(
3383 agent,
3384 controller,
3385 tgt_data.pos.0,
3386 read_data,
3387 Path::Separate,
3388 None,
3389 );
3390 } else {
3391 self.path_toward_target(
3392 agent,
3393 controller,
3394 tgt_data.pos.0,
3395 read_data,
3396 Path::AtTarget,
3397 None,
3398 );
3399 }
3400 }
3401
3402 pub fn handle_quadlow_basic_attack(
3403 &self,
3404 agent: &mut Agent,
3405 controller: &mut Controller,
3406 attack_data: &AttackData,
3407 tgt_data: &TargetData,
3408 read_data: &ReadData,
3409 ) {
3410 enum ActionStateTimers {
3411 TimerQuadLowBasic = 0,
3412 }
3413
3414 if attack_data.angle < 70.0
3415 && attack_data.dist_sqrd < (1.3 * attack_data.min_attack_dist).powi(2)
3416 {
3417 controller.inputs.move_dir = Vec2::zero();
3418 if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 5.0 {
3419 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] = 0.0;
3420 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] > 2.0
3421 {
3422 controller.push_basic_input(InputKind::Secondary);
3423 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] +=
3424 read_data.dt.0;
3425 } else {
3426 controller.push_basic_input(InputKind::Primary);
3427 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBasic as usize] +=
3428 read_data.dt.0;
3429 }
3430 } else {
3431 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3432 Path::Separate
3433 } else {
3434 Path::AtTarget
3435 };
3436 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
3437 }
3438 }
3439
3440 pub fn handle_quadmed_jump_attack(
3441 &self,
3442 agent: &mut Agent,
3443 controller: &mut Controller,
3444 attack_data: &AttackData,
3445 tgt_data: &TargetData,
3446 read_data: &ReadData,
3447 ) {
3448 if attack_data.angle < 90.0
3449 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
3450 {
3451 controller.inputs.move_dir = Vec2::zero();
3452 controller.push_basic_input(InputKind::Secondary);
3453 } else if attack_data.angle < 15.0
3454 && attack_data.dist_sqrd < (5.0 * attack_data.min_attack_dist).powi(2)
3455 {
3456 controller.push_basic_input(InputKind::Ability(0));
3457 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3458 if self
3459 .path_toward_target(
3460 agent,
3461 controller,
3462 tgt_data.pos.0,
3463 read_data,
3464 Path::Separate,
3465 None,
3466 )
3467 .is_some()
3468 && attack_data.angle < 15.0
3469 && entities_have_line_of_sight(
3470 self.pos,
3471 self.body,
3472 self.scale,
3473 tgt_data.pos,
3474 tgt_data.body,
3475 tgt_data.scale,
3476 read_data,
3477 )
3478 {
3479 controller.push_basic_input(InputKind::Primary);
3480 }
3481 } else {
3482 self.path_toward_target(
3483 agent,
3484 controller,
3485 tgt_data.pos.0,
3486 read_data,
3487 Path::AtTarget,
3488 None,
3489 );
3490 }
3491 }
3492
3493 pub fn handle_quadmed_basic_attack(
3494 &self,
3495 agent: &mut Agent,
3496 controller: &mut Controller,
3497 attack_data: &AttackData,
3498 tgt_data: &TargetData,
3499 read_data: &ReadData,
3500 ) {
3501 enum ActionStateTimers {
3502 TimerQuadMedBasic = 0,
3503 }
3504
3505 if attack_data.angle < 90.0 && attack_data.in_min_range() {
3506 controller.inputs.move_dir = Vec2::zero();
3507 if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 2.0 {
3508 controller.push_basic_input(InputKind::Secondary);
3509 agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] +=
3510 read_data.dt.0;
3511 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] < 3.0
3512 {
3513 controller.push_basic_input(InputKind::Primary);
3514 agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] +=
3515 read_data.dt.0;
3516 } else {
3517 agent.combat_state.timers[ActionStateTimers::TimerQuadMedBasic as usize] = 0.0;
3518 }
3519 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3520 self.path_toward_target(
3521 agent,
3522 controller,
3523 tgt_data.pos.0,
3524 read_data,
3525 Path::Separate,
3526 None,
3527 );
3528 } else {
3529 self.path_toward_target(
3530 agent,
3531 controller,
3532 tgt_data.pos.0,
3533 read_data,
3534 Path::AtTarget,
3535 None,
3536 );
3537 }
3538 }
3539
3540 pub fn handle_quadmed_hoof_attack(
3541 &self,
3542 agent: &mut Agent,
3543 controller: &mut Controller,
3544 attack_data: &AttackData,
3545 tgt_data: &TargetData,
3546 read_data: &ReadData,
3547 ) {
3548 const HOOF_ATTACK_RANGE: f32 = 1.0;
3549 const HOOF_ATTACK_ANGLE: f32 = 50.0;
3550
3551 if attack_data.angle < HOOF_ATTACK_ANGLE
3552 && attack_data.dist_sqrd
3553 < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.front_radius())).powi(2)
3554 {
3555 controller.inputs.move_dir = Vec2::zero();
3556 controller.push_basic_input(InputKind::Primary);
3557 } else {
3558 self.path_toward_target(
3559 agent,
3560 controller,
3561 tgt_data.pos.0,
3562 read_data,
3563 Path::AtTarget,
3564 None,
3565 );
3566 }
3567 }
3568
3569 pub fn handle_quadlow_beam_attack(
3570 &self,
3571 agent: &mut Agent,
3572 controller: &mut Controller,
3573 attack_data: &AttackData,
3574 tgt_data: &TargetData,
3575 read_data: &ReadData,
3576 ) {
3577 enum ActionStateTimers {
3578 TimerQuadLowBeam = 0,
3579 }
3580 if attack_data.angle < 90.0
3581 && attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2)
3582 {
3583 controller.inputs.move_dir = Vec2::zero();
3584 controller.push_basic_input(InputKind::Secondary);
3585 } else if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2)
3586 && attack_data.angle < 15.0
3587 {
3588 if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 2.0 {
3589 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3590 .xy()
3591 .rotated_z(0.47 * PI)
3592 .try_normalized()
3593 .unwrap_or_else(Vec2::unit_y);
3594 controller.push_basic_input(InputKind::Primary);
3595 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3596 read_data.dt.0;
3597 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 4.0
3598 && attack_data.angle < 15.0
3599 {
3600 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
3601 .xy()
3602 .rotated_z(-0.47 * PI)
3603 .try_normalized()
3604 .unwrap_or_else(Vec2::unit_y);
3605 controller.push_basic_input(InputKind::Primary);
3606 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3607 read_data.dt.0;
3608 } else if agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] < 6.0
3609 && attack_data.angle < 15.0
3610 {
3611 controller.push_basic_input(InputKind::Ability(0));
3612 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] +=
3613 read_data.dt.0;
3614 } else {
3615 agent.combat_state.timers[ActionStateTimers::TimerQuadLowBeam as usize] = 0.0;
3616 }
3617 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3618 self.path_toward_target(
3619 agent,
3620 controller,
3621 tgt_data.pos.0,
3622 read_data,
3623 Path::Separate,
3624 None,
3625 );
3626 } else {
3627 self.path_toward_target(
3628 agent,
3629 controller,
3630 tgt_data.pos.0,
3631 read_data,
3632 Path::AtTarget,
3633 None,
3634 );
3635 }
3636 }
3637
3638 pub fn handle_organ_aura_attack(
3639 &self,
3640 agent: &mut Agent,
3641 controller: &mut Controller,
3642 attack_data: &AttackData,
3643 _tgt_data: &TargetData,
3644 read_data: &ReadData,
3645 ) {
3646 enum ActionStateTimers {
3647 TimerOrganAura = 0,
3648 }
3649
3650 const ORGAN_AURA_DURATION: f32 = 34.75;
3651 if attack_data.dist_sqrd < (7.0 * attack_data.min_attack_dist).powi(2) {
3652 if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize]
3653 > ORGAN_AURA_DURATION
3654 {
3655 agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] = 0.0;
3656 } else if agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] < 1.0 {
3657 controller.push_basic_input(InputKind::Primary);
3658 agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] +=
3659 read_data.dt.0;
3660 } else {
3661 agent.combat_state.timers[ActionStateTimers::TimerOrganAura as usize] +=
3662 read_data.dt.0;
3663 }
3664 } else {
3665 agent.target = None;
3666 }
3667 }
3668
3669 pub fn handle_theropod_attack(
3670 &self,
3671 agent: &mut Agent,
3672 controller: &mut Controller,
3673 attack_data: &AttackData,
3674 tgt_data: &TargetData,
3675 read_data: &ReadData,
3676 ) {
3677 if attack_data.angle < 90.0 && attack_data.in_min_range() {
3678 controller.inputs.move_dir = Vec2::zero();
3679 controller.push_basic_input(InputKind::Primary);
3680 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
3681 self.path_toward_target(
3682 agent,
3683 controller,
3684 tgt_data.pos.0,
3685 read_data,
3686 Path::Separate,
3687 None,
3688 );
3689 } else {
3690 self.path_toward_target(
3691 agent,
3692 controller,
3693 tgt_data.pos.0,
3694 read_data,
3695 Path::AtTarget,
3696 None,
3697 );
3698 }
3699 }
3700
3701 pub fn handle_turret_attack(
3702 &self,
3703 agent: &mut Agent,
3704 controller: &mut Controller,
3705 attack_data: &AttackData,
3706 tgt_data: &TargetData,
3707 read_data: &ReadData,
3708 ) {
3709 if entities_have_line_of_sight(
3710 self.pos,
3711 self.body,
3712 self.scale,
3713 tgt_data.pos,
3714 tgt_data.body,
3715 tgt_data.scale,
3716 read_data,
3717 ) && attack_data.angle < 15.0
3718 {
3719 controller.push_basic_input(InputKind::Primary);
3720 } else {
3721 agent.target = None;
3722 }
3723 }
3724
3725 pub fn handle_fixed_turret_attack(
3726 &self,
3727 agent: &mut Agent,
3728 controller: &mut Controller,
3729 attack_data: &AttackData,
3730 tgt_data: &TargetData,
3731 read_data: &ReadData,
3732 ) {
3733 controller.inputs.look_dir = self.ori.look_dir();
3734 if entities_have_line_of_sight(
3735 self.pos,
3736 self.body,
3737 self.scale,
3738 tgt_data.pos,
3739 tgt_data.body,
3740 tgt_data.scale,
3741 read_data,
3742 ) && attack_data.angle < 15.0
3743 {
3744 controller.push_basic_input(InputKind::Primary);
3745 } else {
3746 agent.target = None;
3747 }
3748 }
3749
3750 pub fn handle_rotating_turret_attack(
3751 &self,
3752 agent: &mut Agent,
3753 controller: &mut Controller,
3754 tgt_data: &TargetData,
3755 read_data: &ReadData,
3756 ) {
3757 controller.inputs.look_dir = Dir::new(
3758 Quaternion::from_xyzw(self.ori.look_dir().x, self.ori.look_dir().y, 0.0, 0.0)
3759 .rotated_z(6.0 * read_data.dt.0)
3760 .into_vec3()
3761 .try_normalized()
3762 .unwrap_or_default(),
3763 );
3764 if entities_have_line_of_sight(
3765 self.pos,
3766 self.body,
3767 self.scale,
3768 tgt_data.pos,
3769 tgt_data.body,
3770 tgt_data.scale,
3771 read_data,
3772 ) {
3773 controller.push_basic_input(InputKind::Primary);
3774 } else {
3775 agent.target = None;
3776 }
3777 }
3778
3779 pub fn handle_radial_turret_attack(&self, controller: &mut Controller) {
3780 controller.push_basic_input(InputKind::Primary);
3781 }
3782
3783 pub fn handle_fiery_tornado_attack(&self, agent: &mut Agent, controller: &mut Controller) {
3784 enum Conditions {
3785 AuraEmited = 0,
3786 }
3787 if matches!(self.char_state, CharacterState::BasicAura(c) if matches!(c.stage_section, StageSection::Recover))
3788 {
3789 agent.combat_state.conditions[Conditions::AuraEmited as usize] = true;
3790 }
3791 if !agent.combat_state.conditions[Conditions::AuraEmited as usize] {
3793 controller.push_basic_input(InputKind::Secondary);
3794 } else {
3795 controller.push_basic_input(InputKind::Primary);
3797 }
3798 }
3799
3800 pub fn handle_mindflayer_attack(
3801 &self,
3802 agent: &mut Agent,
3803 controller: &mut Controller,
3804 _attack_data: &AttackData,
3805 tgt_data: &TargetData,
3806 read_data: &ReadData,
3807 _rng: &mut impl RngExt,
3808 ) {
3809 enum FCounters {
3810 SummonThreshold = 0,
3811 }
3812 enum Timers {
3813 PositionTimer,
3814 AttackTimer1,
3815 AttackTimer2,
3816 }
3817 enum Conditions {
3818 AttackToggle1,
3819 }
3820 const SUMMON_THRESHOLD: f32 = 0.20;
3821 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3822 agent.combat_state.timers[Timers::PositionTimer as usize] += read_data.dt.0;
3823 agent.combat_state.timers[Timers::AttackTimer1 as usize] += read_data.dt.0;
3824 agent.combat_state.timers[Timers::AttackTimer2 as usize] += read_data.dt.0;
3825 if agent.combat_state.timers[Timers::AttackTimer1 as usize] > 10.0 {
3826 agent.combat_state.timers[Timers::AttackTimer1 as usize] = 0.0
3827 }
3828 agent.combat_state.conditions[Conditions::AttackToggle1 as usize] =
3829 agent.combat_state.timers[Timers::AttackTimer1 as usize] < 5.0;
3830 if matches!(self.char_state, CharacterState::Blink(c) if matches!(c.stage_section, StageSection::Recover))
3831 {
3832 agent.combat_state.timers[Timers::AttackTimer2 as usize] = 0.0
3833 }
3834
3835 let position_timer = agent.combat_state.timers[Timers::PositionTimer as usize];
3836 if position_timer > 60.0 {
3837 agent.combat_state.timers[Timers::PositionTimer as usize] = 0.0;
3838 }
3839 let home = agent.patrol_origin.unwrap_or(self.pos.0);
3840 let p = match position_timer as i32 {
3841 0_i32..=6_i32 => 0,
3842 7_i32..=13_i32 => 2,
3843 14_i32..=20_i32 => 3,
3844 21_i32..=27_i32 => 1,
3845 28_i32..=34_i32 => 4,
3846 35_i32..=47_i32 => 5,
3847 _ => 6,
3848 };
3849 let pos = if p > 5 {
3850 tgt_data.pos.0
3851 } else if p > 3 {
3852 home
3853 } else {
3854 Vec3::new(
3855 home.x + (CARDINALS[p].x * 15) as f32,
3856 home.y + (CARDINALS[p].y * 15) as f32,
3857 home.z,
3858 )
3859 };
3860 if !agent.combat_state.initialized {
3861 agent.combat_state.counters[FCounters::SummonThreshold as usize] =
3864 1.0 - SUMMON_THRESHOLD;
3865 agent.combat_state.initialized = true;
3866 }
3867
3868 if position_timer > 55.0
3869 && health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
3870 {
3871 controller.push_basic_input(InputKind::Ability(2));
3873
3874 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
3875 {
3876 agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
3877 SUMMON_THRESHOLD;
3878 }
3879 } else if p > 5 {
3880 if pos.distance_squared(self.pos.0) > 20.0_f32.powi(2) {
3881 controller.push_action(ControlAction::StartInput {
3883 input: InputKind::Ability(0),
3884 target_entity: None,
3885 select_pos: Some(pos),
3886 });
3887 } else {
3888 controller.push_basic_input(InputKind::Ability(4))
3889 }
3890 } else if p > 4 {
3891 self.path_toward_target(
3893 agent,
3894 controller,
3895 tgt_data.pos.0,
3896 read_data,
3897 Path::AtTarget,
3898 None,
3899 );
3900
3901 if agent.combat_state.conditions[Conditions::AttackToggle1 as usize] {
3902 controller.push_basic_input(InputKind::Primary);
3903 } else {
3904 controller.push_basic_input(InputKind::Ability(1))
3905 }
3906 } else {
3907 if pos.distance_squared(self.pos.0) > 5.0_f32.powi(2) {
3909 controller.push_action(ControlAction::StartInput {
3910 input: InputKind::Ability(0),
3911 target_entity: None,
3912 select_pos: Some(pos),
3913 });
3914 } else if agent.combat_state.timers[Timers::AttackTimer2 as usize] < 4.0 {
3915 controller.push_basic_input(InputKind::Secondary);
3916 } else {
3917 controller.push_basic_input(InputKind::Ability(3))
3918 }
3919 }
3920 }
3921
3922 pub fn handle_forgemaster_attack(
3923 &self,
3924 agent: &mut Agent,
3925 controller: &mut Controller,
3926 attack_data: &AttackData,
3927 tgt_data: &TargetData,
3928 read_data: &ReadData,
3929 ) {
3930 const MELEE_RANGE: f32 = 6.0;
3931 const MID_RANGE: f32 = 25.0;
3932 const SUMMON_THRESHOLD: f32 = 0.2;
3933
3934 enum FCounters {
3935 SummonThreshold = 0,
3936 }
3937 enum Timers {
3938 AttackRand = 0,
3939 }
3940 if agent.combat_state.timers[Timers::AttackRand as usize] > 10.0 {
3941 agent.combat_state.timers[Timers::AttackRand as usize] = 0.0;
3942 }
3943
3944 let line_of_sight_with_target = || {
3945 entities_have_line_of_sight(
3946 self.pos,
3947 self.body,
3948 self.scale,
3949 tgt_data.pos,
3950 tgt_data.body,
3951 tgt_data.scale,
3952 read_data,
3953 )
3954 };
3955 let home = agent.patrol_origin.unwrap_or(self.pos.0);
3956 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
3957 if (5f32.powi(2)..100f32.powi(2)).contains(&home.distance_squared(self.pos.0)) {
3960 controller.push_action(ControlAction::StartInput {
3961 input: InputKind::Ability(5),
3962 target_entity: None,
3963 select_pos: Some(home),
3964 });
3965 } else if !agent.combat_state.initialized {
3966 agent.combat_state.counters[FCounters::SummonThreshold as usize] =
3969 1.0 - SUMMON_THRESHOLD;
3970 agent.combat_state.initialized = true;
3971 } else if health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
3972 {
3973 controller.push_basic_input(InputKind::Ability(0));
3975
3976 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
3977 {
3978 agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
3979 SUMMON_THRESHOLD;
3980 }
3981 } else {
3982 if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
3984 if agent.combat_state.timers[Timers::AttackRand as usize] < 3.5 {
3985 controller.push_basic_input(InputKind::Secondary);
3987 } else {
3988 controller.push_basic_input(InputKind::Ability(3));
3990 }
3991 } else if attack_data.dist_sqrd < MID_RANGE.powi(2) && line_of_sight_with_target() {
3994 if agent.combat_state.timers[Timers::AttackRand as usize] > 6.5 {
3995 controller.push_basic_input(InputKind::Ability(1));
3996 } else if agent.combat_state.timers[Timers::AttackRand as usize] > 3.5 {
3997 controller.push_basic_input(InputKind::Ability(3));
3999 } else if agent.combat_state.timers[Timers::AttackRand as usize] > 2.5 {
4000 controller.push_basic_input(InputKind::Primary);
4002 } else {
4003 controller.push_basic_input(InputKind::Ability(2));
4005 }
4006 } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
4009 if agent.combat_state.timers[Timers::AttackRand as usize] > 6.5 {
4010 controller.push_basic_input(InputKind::Ability(4));
4011 } else {
4012 controller.push_basic_input(InputKind::Primary);
4014 }
4015 }
4016 agent.combat_state.timers[Timers::AttackRand as usize] += read_data.dt.0;
4017 }
4018 self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
4019 }
4020
4021 pub fn handle_flamekeeper_attack(
4022 &self,
4023 agent: &mut Agent,
4024 controller: &mut Controller,
4025 attack_data: &AttackData,
4026 tgt_data: &TargetData,
4027 read_data: &ReadData,
4028 ) {
4029 const MELEE_RANGE: f32 = 6.0;
4030 const MID_RANGE: f32 = 25.0;
4031 const SUMMON_THRESHOLD: f32 = 0.2;
4032
4033 enum FCounters {
4034 SummonThreshold = 0,
4035 }
4036 enum Timers {
4037 AttackRand = 0,
4038 }
4039 if agent.combat_state.timers[Timers::AttackRand as usize] > 5.0 {
4040 agent.combat_state.timers[Timers::AttackRand as usize] = 0.0;
4041 }
4042
4043 let line_of_sight_with_target = || {
4044 entities_have_line_of_sight(
4045 self.pos,
4046 self.body,
4047 self.scale,
4048 tgt_data.pos,
4049 tgt_data.body,
4050 tgt_data.scale,
4051 read_data,
4052 )
4053 };
4054 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4055 if !agent.combat_state.initialized {
4058 agent.combat_state.counters[FCounters::SummonThreshold as usize] =
4059 1.0 - SUMMON_THRESHOLD;
4060 agent.combat_state.initialized = true;
4061 } else if health_fraction < agent.combat_state.counters[FCounters::SummonThreshold as usize]
4062 {
4063 controller.push_basic_input(InputKind::Ability(0));
4065 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
4066 {
4067 agent.combat_state.counters[FCounters::SummonThreshold as usize] -=
4068 SUMMON_THRESHOLD;
4069 }
4070 } else {
4071 if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4073 if agent.combat_state.timers[Timers::AttackRand as usize] < 3.5 {
4074 controller.push_basic_input(InputKind::Secondary);
4076 } else {
4077 controller.push_basic_input(InputKind::Ability(2));
4079 }
4080 } else if attack_data.dist_sqrd < MID_RANGE.powi(2) && line_of_sight_with_target() {
4082 if agent.combat_state.timers[Timers::AttackRand as usize] > 3.5 {
4083 controller.push_basic_input(InputKind::Ability(2));
4085 } else if agent.combat_state.timers[Timers::AttackRand as usize] > 2.5 {
4086 controller.push_basic_input(InputKind::Ability(3));
4088 } else {
4089 controller.push_basic_input(InputKind::Ability(1));
4091 }
4092 } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
4094 controller.push_basic_input(InputKind::Primary);
4096 }
4097 self.path_toward_target(
4098 agent,
4099 controller,
4100 tgt_data.pos.0,
4101 read_data,
4102 Path::AtTarget,
4103 None,
4104 );
4105 agent.combat_state.timers[Timers::AttackRand as usize] += read_data.dt.0;
4106 }
4107 }
4108
4109 pub fn handle_birdlarge_fire_attack(
4110 &self,
4111 agent: &mut Agent,
4112 controller: &mut Controller,
4113 attack_data: &AttackData,
4114 tgt_data: &TargetData,
4115 read_data: &ReadData,
4116 _rng: &mut impl RngExt,
4117 ) {
4118 const PHOENIX_HEAL_THRESHOLD: f32 = 0.20;
4119
4120 enum Conditions {
4121 Healed = 0,
4122 }
4123 enum ActionStateTimers {
4124 AttackTimer1,
4125 AttackTimer2,
4126 WaterTimer,
4127 }
4128
4129 let attack_timer_1 =
4130 if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 2.0 {
4131 0
4132 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 4.0 {
4133 1
4134 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] < 6.0 {
4135 2
4136 } else {
4137 3
4138 };
4139 agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] += read_data.dt.0;
4140 if agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] > 8.0 {
4141 agent.combat_state.timers[ActionStateTimers::AttackTimer1 as usize] = 0.0;
4143 }
4144 let (attack_timer_2, speed) =
4145 if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 3.0 {
4146 (0, 2.0)
4148 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 6.0 {
4149 (1, 2.0)
4151 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 9.0 {
4152 (0, 3.0)
4154 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 16.0 {
4155 (2, 1.0)
4157 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] < 20.0 {
4158 (5, 20.0)
4160 } else {
4161 (3, 1.0)
4163 };
4164 agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] += read_data.dt.0;
4165 if agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] > 28.0 {
4166 agent.combat_state.timers[ActionStateTimers::AttackTimer2 as usize] = 0.0;
4168 }
4169 let dir_to_target = ((tgt_data.pos.0 + Vec3::unit_z() * 1.5) - self.pos.0)
4171 .try_normalized()
4172 .unwrap_or_else(Vec3::zero);
4173 controller.inputs.move_dir = dir_to_target.xy() * speed;
4174
4175 controller.push_basic_input(InputKind::Fly);
4177 if matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { .. })) {
4183 agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] = 2.0;
4184 };
4185 if agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] > 0.0 {
4186 agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] -= read_data.dt.0;
4187 if agent.combat_state.timers[ActionStateTimers::WaterTimer as usize] > 1.0 {
4188 controller.inputs.move_z = 1.0
4189 } else {
4190 controller.push_basic_input(InputKind::Ability(3))
4192 }
4193 } else if self.physics_state.on_ground.is_some() {
4194 controller.push_basic_input(InputKind::Jump);
4195 } else {
4196 let mut maintain_altitude = |set_point| {
4199 let alt = read_data
4200 .terrain
4201 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0))
4202 .until(Block::is_solid)
4203 .cast()
4204 .0;
4205 let error = set_point - alt;
4206 controller.inputs.move_z = error;
4207 };
4208 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4210 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4211 {
4212 agent.combat_state.conditions[Conditions::Healed as usize] = true;
4213 }
4214 if !agent.combat_state.conditions[Conditions::Healed as usize]
4215 && PHOENIX_HEAL_THRESHOLD > health_fraction
4216 {
4217 controller.push_basic_input(InputKind::Ability(4));
4218 } else if (tgt_data.pos.0 - self.pos.0).xy().magnitude_squared() > (35.0_f32).powi(2) {
4219 maintain_altitude(2.0);
4221 controller.push_basic_input(InputKind::Ability(3))
4222 } else {
4223 match attack_timer_2 {
4224 0 => maintain_altitude(3.0),
4225 1 => {
4226 controller.push_basic_input(InputKind::Ability(1));
4228 },
4229 2 => {
4230 controller.push_basic_input(InputKind::Ability(2));
4232 },
4233 3 => {
4234 if attack_data.dist_sqrd < 4.0_f32.powi(2) && attack_data.angle < 150.0 {
4235 match attack_timer_1 {
4237 1 => {
4238 controller.push_basic_input(InputKind::Primary);
4240 },
4241 3 => {
4242 controller.push_basic_input(InputKind::Secondary)
4244 },
4245 _ => {
4246 controller.push_basic_input(InputKind::Ability(0))
4248 },
4249 }
4250 } else {
4251 match attack_timer_1 {
4252 0 | 2 => {
4253 maintain_altitude(2.0);
4254 },
4255 _ => {
4256 controller.push_basic_input(InputKind::Ability(3))
4258 },
4259 }
4260 }
4261 },
4262 _ => {
4263 maintain_altitude(2.0);
4264 },
4265 }
4266 }
4267 }
4268 }
4269
4270 pub fn handle_wyvern_attack(
4271 &self,
4272 agent: &mut Agent,
4273 controller: &mut Controller,
4274 attack_data: &AttackData,
4275 tgt_data: &TargetData,
4276 read_data: &ReadData,
4277 _rng: &mut impl RngExt,
4278 ) {
4279 enum ActionStateTimers {
4280 AttackTimer = 0,
4281 }
4282 controller.push_cancel_input(InputKind::Fly);
4284 if attack_data.dist_sqrd > 30.0_f32.powi(2) {
4285 if entities_have_line_of_sight(
4286 self.pos,
4287 self.body,
4288 self.scale,
4289 tgt_data.pos,
4290 tgt_data.body,
4291 tgt_data.scale,
4292 read_data,
4293 ) && attack_data.angle < 15.0
4294 {
4295 controller.push_basic_input(InputKind::Primary);
4296 }
4297 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
4298 &*read_data.terrain,
4299 self.pos.0,
4300 self.vel.0,
4301 tgt_data.pos.0,
4302 TraversalConfig {
4303 min_tgt_dist: 1.25,
4304 ..self.traversal_config
4305 },
4306 &read_data.time,
4307 ) {
4308 self.unstuck_if(stuck, controller);
4309 controller.inputs.move_dir =
4310 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
4311 if (self.pos.0.z - tgt_data.pos.0.z) < 35.0 {
4312 controller.push_basic_input(InputKind::Fly);
4313 controller.inputs.move_z = 0.2;
4314 }
4315 }
4316 } else if !read_data
4317 .terrain
4318 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0))
4319 .until(Block::is_solid)
4320 .cast()
4321 .1
4322 .map_or(true, |b| b.is_some())
4323 {
4324 controller.push_basic_input(InputKind::Fly);
4328 let move_dir = tgt_data.pos.0 - self.pos.0;
4329 controller.inputs.move_dir =
4330 move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0;
4331 controller.inputs.move_z = move_dir.z - 0.5;
4332 if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4333 && attack_data.angle < 15.0
4334 {
4335 controller.push_basic_input(InputKind::Primary);
4336 }
4337 } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) {
4338 self.path_toward_target(
4339 agent,
4340 controller,
4341 tgt_data.pos.0,
4342 read_data,
4343 Path::Separate,
4344 None,
4345 );
4346 } else if attack_data.angle < 15.0 {
4347 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
4348 controller.push_basic_input(InputKind::Ability(1));
4350 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0 {
4351 controller.push_basic_input(InputKind::Ability(0));
4353 } else {
4354 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
4355 }
4356 self.path_toward_target(
4358 agent,
4359 controller,
4360 tgt_data.pos.0,
4361 read_data,
4362 Path::Separate,
4363 Some(0.5),
4364 );
4365 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
4366 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 9.0
4367 && attack_data.angle < 90.0
4368 && attack_data.in_min_range()
4369 {
4370 controller.push_basic_input(InputKind::Secondary);
4372 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
4373 } else {
4374 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
4376 self.path_toward_target(
4378 agent,
4379 controller,
4380 tgt_data.pos.0,
4381 read_data,
4382 Path::Separate,
4383 None,
4384 );
4385 }
4386 }
4387
4388 pub fn handle_birdlarge_breathe_attack(
4389 &self,
4390 agent: &mut Agent,
4391 controller: &mut Controller,
4392 attack_data: &AttackData,
4393 tgt_data: &TargetData,
4394 read_data: &ReadData,
4395 rng: &mut impl RngExt,
4396 ) {
4397 enum ActionStateTimers {
4398 TimerBirdLargeBreathe = 0,
4399 }
4400
4401 controller.push_cancel_input(InputKind::Fly);
4403 if attack_data.dist_sqrd > 30.0_f32.powi(2) {
4404 if rng.random_bool(0.05)
4405 && entities_have_line_of_sight(
4406 self.pos,
4407 self.body,
4408 self.scale,
4409 tgt_data.pos,
4410 tgt_data.body,
4411 tgt_data.scale,
4412 read_data,
4413 )
4414 && attack_data.angle < 15.0
4415 {
4416 controller.push_basic_input(InputKind::Primary);
4417 }
4418 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
4419 &*read_data.terrain,
4420 self.pos.0,
4421 self.vel.0,
4422 tgt_data.pos.0,
4423 TraversalConfig {
4424 min_tgt_dist: 1.25,
4425 ..self.traversal_config
4426 },
4427 &read_data.time,
4428 ) {
4429 self.unstuck_if(stuck, controller);
4430 controller.inputs.move_dir =
4431 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
4432 if (self.pos.0.z - tgt_data.pos.0.z) < 20.0 {
4433 controller.push_basic_input(InputKind::Fly);
4434 controller.inputs.move_z = 1.0;
4435 }
4436 }
4437 } else if !read_data
4438 .terrain
4439 .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 2.0))
4440 .until(Block::is_solid)
4441 .cast()
4442 .1
4443 .map_or(true, |b| b.is_some())
4444 {
4445 controller.push_basic_input(InputKind::Fly);
4449 let move_dir = tgt_data.pos.0 - self.pos.0;
4450 controller.inputs.move_dir =
4451 move_dir.xy().try_normalized().unwrap_or_else(Vec2::zero) * 2.0;
4452 controller.inputs.move_z = move_dir.z - 0.5;
4453 if rng.random_bool(0.05)
4454 && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4455 && attack_data.angle < 15.0
4456 {
4457 controller.push_basic_input(InputKind::Primary);
4458 }
4459 } else if rng.random_bool(0.05)
4460 && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4461 && attack_data.angle < 15.0
4462 {
4463 controller.push_basic_input(InputKind::Primary);
4464 } else if rng.random_bool(0.5)
4465 && (self.pos.0.z - tgt_data.pos.0.z) < 15.0
4466 && attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2)
4467 {
4468 controller.push_basic_input(InputKind::Fly);
4469 controller.inputs.move_z = 1.0;
4470 } else if attack_data.dist_sqrd > (3.0 * attack_data.min_attack_dist).powi(2) {
4471 self.path_toward_target(
4472 agent,
4473 controller,
4474 tgt_data.pos.0,
4475 read_data,
4476 Path::Separate,
4477 None,
4478 );
4479 } else if self.energy.current() > 60.0
4480 && agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 3.0
4481 && attack_data.angle < 15.0
4482 {
4483 controller.push_basic_input(InputKind::Ability(0));
4485 self.path_toward_target(
4487 agent,
4488 controller,
4489 tgt_data.pos.0,
4490 read_data,
4491 Path::Separate,
4492 Some(0.5),
4493 );
4494 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] +=
4495 read_data.dt.0;
4496 } else if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] < 6.0
4497 && attack_data.angle < 90.0
4498 && attack_data.in_min_range()
4499 {
4500 controller.push_basic_input(InputKind::Secondary);
4502 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] +=
4503 read_data.dt.0;
4504 } else {
4505 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBreathe as usize] = 0.0;
4507 self.path_toward_target(
4509 agent,
4510 controller,
4511 tgt_data.pos.0,
4512 read_data,
4513 Path::Separate,
4514 None,
4515 );
4516 }
4517 }
4518
4519 pub fn handle_birdlarge_basic_attack(
4520 &self,
4521 agent: &mut Agent,
4522 controller: &mut Controller,
4523 attack_data: &AttackData,
4524 tgt_data: &TargetData,
4525 read_data: &ReadData,
4526 ) {
4527 enum ActionStateTimers {
4528 TimerBirdLargeBasic = 0,
4529 }
4530
4531 enum ActionStateConditions {
4532 ConditionBirdLargeBasic = 0, }
4535
4536 const BIRD_ATTACK_RANGE: f32 = 4.0;
4537 const BIRD_CHARGE_DISTANCE: f32 = 15.0;
4538 let bird_attack_distance = self.body.map_or(0.0, |b| b.max_radius()) + BIRD_ATTACK_RANGE;
4539 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] +=
4541 read_data.dt.0;
4542 if agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] > 8.0 {
4543 controller.push_basic_input(InputKind::Secondary);
4545 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
4546 {
4547 agent.combat_state.timers[ActionStateTimers::TimerBirdLargeBasic as usize] = 0.0;
4549 }
4550 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4551 {
4552 controller.push_basic_input(InputKind::Ability(0));
4554 } else if matches!(self.char_state, CharacterState::ComboMelee2(c) if matches!(c.stage_section, StageSection::Recover))
4555 {
4556 controller.push_basic_input(InputKind::Primary);
4558 } else if attack_data.dist_sqrd > BIRD_CHARGE_DISTANCE.powi(2) {
4559 if attack_data.angle < 60.0 {
4561 controller.push_basic_input(InputKind::Ability(0));
4562 }
4563 } else if attack_data.dist_sqrd < bird_attack_distance.powi(2) {
4564 controller.push_basic_input(InputKind::Primary);
4566 agent.combat_state.conditions
4567 [ActionStateConditions::ConditionBirdLargeBasic as usize] = true;
4568 }
4569 self.path_toward_target(
4571 agent,
4572 controller,
4573 tgt_data.pos.0,
4574 read_data,
4575 Path::Separate,
4576 None,
4577 );
4578 }
4579
4580 pub fn handle_arthropod_ranged_attack(
4581 &self,
4582 agent: &mut Agent,
4583 controller: &mut Controller,
4584 attack_data: &AttackData,
4585 tgt_data: &TargetData,
4586 read_data: &ReadData,
4587 ) {
4588 enum ActionStateTimers {
4589 TimerArthropodRanged = 0,
4590 }
4591
4592 agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] +=
4593 read_data.dt.0;
4594 if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] > 6.0
4595 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
4596 {
4597 controller.inputs.move_dir = Vec2::zero();
4598 controller.push_basic_input(InputKind::Secondary);
4599 if matches!(self.char_state,
4601 CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. })
4602 | CharacterState::SelfBuff(self_buff::Data { stage_section, .. })
4603 if matches!(stage_section, StageSection::Recover))
4604 {
4605 agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize] = 0.0;
4606 }
4607 } else if attack_data.dist_sqrd < (2.5 * attack_data.min_attack_dist).powi(2)
4608 && attack_data.angle < 90.0
4609 {
4610 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4611 .xy()
4612 .try_normalized()
4613 .unwrap_or_else(Vec2::unit_y)
4614 * if attack_data.in_min_range() { 0.3 } else { 1.0 };
4616 controller.push_basic_input(InputKind::Primary);
4617 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
4618 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
4619 &*read_data.terrain,
4620 self.pos.0,
4621 self.vel.0,
4622 tgt_data.pos.0,
4623 TraversalConfig {
4624 min_tgt_dist: 1.25,
4625 ..self.traversal_config
4626 },
4627 &read_data.time,
4628 ) {
4629 self.unstuck_if(stuck, controller);
4630 if attack_data.angle < 15.0
4631 && entities_have_line_of_sight(
4632 self.pos,
4633 self.body,
4634 self.scale,
4635 tgt_data.pos,
4636 tgt_data.body,
4637 tgt_data.scale,
4638 read_data,
4639 )
4640 {
4641 if agent.combat_state.timers[ActionStateTimers::TimerArthropodRanged as usize]
4642 > 5.0
4643 {
4644 agent.combat_state.timers
4645 [ActionStateTimers::TimerArthropodRanged as usize] = 0.0;
4646 } else if agent.combat_state.timers
4647 [ActionStateTimers::TimerArthropodRanged as usize]
4648 > 2.5
4649 {
4650 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4651 .xy()
4652 .rotated_z(1.75 * PI)
4653 .try_normalized()
4654 .unwrap_or_else(Vec2::zero)
4655 * speed;
4656 agent.combat_state.timers
4657 [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0;
4658 } else {
4659 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
4660 .xy()
4661 .rotated_z(0.25 * PI)
4662 .try_normalized()
4663 .unwrap_or_else(Vec2::zero)
4664 * speed;
4665 agent.combat_state.timers
4666 [ActionStateTimers::TimerArthropodRanged as usize] += read_data.dt.0;
4667 }
4668 controller.push_basic_input(InputKind::Ability(0));
4669 self.jump_if(bearing.z > 1.5, controller);
4670 controller.inputs.move_z = bearing.z;
4671 } else {
4672 controller.inputs.move_dir =
4673 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
4674 self.jump_if(bearing.z > 1.5, controller);
4675 controller.inputs.move_z = bearing.z;
4676 }
4677 } else {
4678 agent.target = None;
4679 }
4680 } else {
4681 self.path_toward_target(
4682 agent,
4683 controller,
4684 tgt_data.pos.0,
4685 read_data,
4686 Path::AtTarget,
4687 None,
4688 );
4689 }
4690 }
4691
4692 pub fn handle_arthropod_ambush_attack(
4693 &self,
4694 agent: &mut Agent,
4695 controller: &mut Controller,
4696 attack_data: &AttackData,
4697 tgt_data: &TargetData,
4698 read_data: &ReadData,
4699 rng: &mut impl RngExt,
4700 ) {
4701 enum ActionStateTimers {
4702 TimersArthropodAmbush = 0,
4703 }
4704
4705 agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] +=
4706 read_data.dt.0;
4707 if agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] > 12.0
4708 && attack_data.dist_sqrd < (1.5 * attack_data.min_attack_dist).powi(2)
4709 {
4710 controller.inputs.move_dir = Vec2::zero();
4711 controller.push_basic_input(InputKind::Secondary);
4712 if matches!(self.char_state,
4714 CharacterState::SpriteSummon(sprite_summon::Data { stage_section, .. })
4715 | CharacterState::SelfBuff(self_buff::Data { stage_section, .. })
4716 if matches!(stage_section, StageSection::Recover))
4717 {
4718 agent.combat_state.timers[ActionStateTimers::TimersArthropodAmbush as usize] = 0.0;
4719 }
4720 } else if attack_data.angle < 90.0
4721 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
4722 {
4723 controller.inputs.move_dir = Vec2::zero();
4724 controller.push_basic_input(InputKind::Primary);
4725 } else if rng.random_bool(0.01)
4726 && attack_data.angle < 60.0
4727 && attack_data.dist_sqrd > (2.0 * attack_data.min_attack_dist).powi(2)
4728 {
4729 controller.push_basic_input(InputKind::Ability(0));
4730 } else {
4731 self.path_toward_target(
4732 agent,
4733 controller,
4734 tgt_data.pos.0,
4735 read_data,
4736 Path::AtTarget,
4737 None,
4738 );
4739 }
4740 }
4741
4742 pub fn handle_arthropod_melee_attack(
4743 &self,
4744 agent: &mut Agent,
4745 controller: &mut Controller,
4746 attack_data: &AttackData,
4747 tgt_data: &TargetData,
4748 read_data: &ReadData,
4749 ) {
4750 enum ActionStateTimers {
4751 TimersArthropodMelee = 0,
4752 }
4753 agent.combat_state.timers[ActionStateTimers::TimersArthropodMelee as usize] +=
4754 read_data.dt.0;
4755 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4756 {
4757 controller.push_basic_input(InputKind::Secondary);
4759 } else if attack_data.dist_sqrd > (2.5 * attack_data.min_attack_dist).powi(2) {
4760 if attack_data.angle < 60.0 {
4762 controller.push_basic_input(InputKind::Secondary);
4763 }
4764 } else if attack_data.angle < 90.0
4765 && attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2)
4766 {
4767 controller.inputs.move_dir = Vec2::zero();
4768 controller.push_basic_input(InputKind::Primary);
4769 } else {
4770 self.path_toward_target(
4771 agent,
4772 controller,
4773 tgt_data.pos.0,
4774 read_data,
4775 Path::AtTarget,
4776 None,
4777 );
4778 }
4779 }
4780
4781 pub fn handle_minotaur_attack(
4782 &self,
4783 agent: &mut Agent,
4784 controller: &mut Controller,
4785 attack_data: &AttackData,
4786 tgt_data: &TargetData,
4787 read_data: &ReadData,
4788 ) {
4789 const MINOTAUR_FRENZY_THRESHOLD: f32 = 0.5;
4790 const MINOTAUR_ATTACK_RANGE: f32 = 5.0;
4791 const MINOTAUR_CHARGE_DISTANCE: f32 = 15.0;
4792
4793 enum ActionStateFCounters {
4794 FCounterMinotaurAttack = 0,
4795 }
4796
4797 enum ActionStateConditions {
4798 ConditionJustCrippledOrCleaved = 0,
4799 }
4800
4801 enum Conditions {
4802 AttackToggle,
4803 }
4804
4805 enum Timers {
4806 CheeseTimer,
4807 CanSeeTarget,
4808 Reposition,
4809 }
4810
4811 let minotaur_attack_distance =
4812 self.body.map_or(0.0, |b| b.max_radius()) + MINOTAUR_ATTACK_RANGE;
4813 let health_fraction = self.health.map_or(1.0, |h| h.fraction());
4814 let home = agent.patrol_origin.unwrap_or(self.pos.0);
4815 let center = Vec2::new(home.x + 50.0, home.y + 75.0);
4816 let cheesed_from_above = tgt_data.pos.0.z > self.pos.0.z + 4.0;
4817 let center_cheesed = (center - self.pos.0.xy()).magnitude_squared() < 16.0_f32.powi(2);
4818 let pillar_cheesed = (center - tgt_data.pos.0.xy()).magnitude_squared() < 16.0_f32.powi(2);
4819 let cheesed = (pillar_cheesed || center_cheesed)
4820 && agent.combat_state.timers[Timers::CheeseTimer as usize] > 4.0;
4821 agent.combat_state.timers[Timers::CheeseTimer as usize] += read_data.dt.0;
4822 agent.combat_state.timers[Timers::CanSeeTarget as usize] += read_data.dt.0;
4823 agent.combat_state.timers[Timers::Reposition as usize] += read_data.dt.0;
4824 if agent.combat_state.timers[Timers::Reposition as usize] > 20.0 {
4825 agent.combat_state.timers[Timers::Reposition as usize] = 0.0;
4826 }
4827 let line_of_sight_with_target = || {
4828 entities_have_line_of_sight(
4829 self.pos,
4830 self.body,
4831 self.scale,
4832 tgt_data.pos,
4833 tgt_data.body,
4834 tgt_data.scale,
4835 read_data,
4836 )
4837 };
4838 if !line_of_sight_with_target() {
4839 agent.combat_state.timers[Timers::CanSeeTarget as usize] = 0.0;
4840 };
4841 let remote_spikes_action = || ControlAction::StartInput {
4842 input: InputKind::Ability(3),
4843 target_entity: None,
4844 select_pos: Some(tgt_data.pos.0),
4845 };
4846 if agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4848 < MINOTAUR_FRENZY_THRESHOLD
4849 && health_fraction > MINOTAUR_FRENZY_THRESHOLD
4850 {
4851 agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize] =
4852 MINOTAUR_FRENZY_THRESHOLD;
4853 }
4854 if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
4855 {
4856 agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
4857 }
4858 if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
4859 {
4860 agent.combat_state.conditions[Conditions::AttackToggle as usize] = false;
4861 if agent.combat_state.timers[Timers::CheeseTimer as usize] > 10.0 {
4862 agent.combat_state.timers[Timers::CheeseTimer as usize] = 0.0;
4863 }
4864 }
4865 if cheesed_from_above || cheesed {
4867 if agent.combat_state.conditions[Conditions::AttackToggle as usize] {
4868 controller.push_basic_input(InputKind::Ability(2));
4869 } else {
4870 controller.push_action(remote_spikes_action());
4871 }
4872 if center_cheesed {
4874 let dir_index = match agent.combat_state.timers[Timers::Reposition as usize] as i32
4876 {
4877 0_i32..5_i32 => 0,
4878 5_i32..10_i32 => 1,
4879 10_i32..15_i32 => 2,
4880 _ => 3,
4881 };
4882 let goto = Vec3::new(
4883 center.x + (CARDINALS[dir_index].x * 25) as f32,
4884 center.y + (CARDINALS[dir_index].y * 25) as f32,
4885 tgt_data.pos.0.z,
4886 );
4887 self.path_toward_target(
4888 agent,
4889 controller,
4890 goto,
4891 read_data,
4892 Path::AtTarget,
4893 (attack_data.dist_sqrd
4894 < (attack_data.min_attack_dist + MINOTAUR_ATTACK_RANGE / 3.0).powi(2))
4895 .then_some(0.1),
4896 );
4897 }
4898 } else if health_fraction
4899 < agent.combat_state.counters[ActionStateFCounters::FCounterMinotaurAttack as usize]
4900 {
4901 controller.push_basic_input(InputKind::Ability(1));
4903 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
4904 {
4905 agent.combat_state.counters
4906 [ActionStateFCounters::FCounterMinotaurAttack as usize] = 0.0;
4907 }
4908 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4909 {
4910 controller.push_basic_input(InputKind::Ability(0));
4912 } else if matches!(self.char_state, CharacterState::ChargedMelee(c) if matches!(c.stage_section, StageSection::Charge) && c.timer < c.static_data.charge_duration)
4913 {
4914 controller.push_basic_input(InputKind::Primary);
4916 } else if attack_data.dist_sqrd > MINOTAUR_CHARGE_DISTANCE.powi(2) {
4917 if attack_data.angle < 60.0 {
4919 controller.push_basic_input(InputKind::Ability(0));
4920 }
4921 } else if attack_data.dist_sqrd < minotaur_attack_distance.powi(2) {
4922 if agent.combat_state.conditions
4923 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize]
4924 && !self.char_state.is_attack()
4925 {
4926 controller.push_basic_input(InputKind::Secondary);
4928 agent.combat_state.conditions
4929 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = false;
4930 } else if !self.char_state.is_attack() {
4931 controller.push_basic_input(InputKind::Primary);
4933 agent.combat_state.conditions
4934 [ActionStateConditions::ConditionJustCrippledOrCleaved as usize] = true;
4935 }
4936 }
4937 if cheesed_from_above {
4939 self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
4940 } else if agent.combat_state.timers[Timers::CanSeeTarget as usize] > 2.0
4942 || (3.0..18.0).contains(&(self.pos.0.y - home.y))
4944 {
4945 self.path_toward_target(
4946 agent,
4947 controller,
4948 tgt_data.pos.0,
4949 read_data,
4950 Path::AtTarget,
4951 (attack_data.dist_sqrd
4952 < (attack_data.min_attack_dist + MINOTAUR_ATTACK_RANGE / 3.0).powi(2))
4953 .then_some(0.1),
4954 );
4955 }
4956 }
4957
4958 pub fn handle_cyclops_attack(
4959 &self,
4960 agent: &mut Agent,
4961 controller: &mut Controller,
4962 attack_data: &AttackData,
4963 tgt_data: &TargetData,
4964 read_data: &ReadData,
4965 ) {
4966 const CYCLOPS_MELEE_RANGE: f32 = 9.0;
4968 const CYCLOPS_FIRE_RANGE: f32 = 30.0;
4970 const CYCLOPS_CHARGE_RANGE: f32 = 18.0;
4972 const SHOCKWAVE_THRESHOLD: f32 = 0.6;
4974
4975 enum FCounters {
4976 ShockwaveThreshold = 0,
4977 }
4978 enum Timers {
4979 AttackChange = 0,
4980 }
4981
4982 if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
4983 agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
4984 }
4985
4986 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
4987 if !agent.combat_state.initialized {
4990 agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] =
4991 1.0 - SHOCKWAVE_THRESHOLD;
4992 agent.combat_state.initialized = true;
4993 } else if health_fraction
4994 < agent.combat_state.counters[FCounters::ShockwaveThreshold as usize]
4995 {
4996 controller.push_basic_input(InputKind::Ability(2));
4998
4999 if matches!(self.char_state, CharacterState::SelfBuff(c) if matches!(c.stage_section, StageSection::Recover))
5000 {
5001 agent.combat_state.counters[FCounters::ShockwaveThreshold as usize] -=
5002 SHOCKWAVE_THRESHOLD;
5003 }
5004 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5005 {
5006 controller.push_basic_input(InputKind::Ability(0));
5008 } else if attack_data.dist_sqrd > CYCLOPS_FIRE_RANGE.powi(2) {
5009 controller.push_basic_input(InputKind::Ability(1));
5011 } else if attack_data.dist_sqrd > CYCLOPS_CHARGE_RANGE.powi(2) {
5012 controller.push_basic_input(InputKind::Secondary);
5014 } else if attack_data.dist_sqrd < CYCLOPS_MELEE_RANGE.powi(2) {
5015 if attack_data.angle < 60.0 {
5016 controller.push_basic_input(InputKind::Primary);
5018 } else if attack_data.angle > 60.0 {
5019 controller.push_basic_input(InputKind::Ability(0));
5021 }
5022 }
5023
5024 self.path_toward_target(
5026 agent,
5027 controller,
5028 tgt_data.pos.0,
5029 read_data,
5030 Path::AtTarget,
5031 (attack_data.dist_sqrd
5032 < (attack_data.min_attack_dist + CYCLOPS_MELEE_RANGE / 2.0).powi(2))
5033 .then_some(0.1),
5034 );
5035 }
5036
5037 pub fn handle_dullahan_attack(
5038 &self,
5039 agent: &mut Agent,
5040 controller: &mut Controller,
5041 attack_data: &AttackData,
5042 tgt_data: &TargetData,
5043 read_data: &ReadData,
5044 ) {
5045 const MELEE_RANGE: f32 = 9.0;
5047 const LONG_RANGE: f32 = 30.0;
5049 const HP_THRESHOLD: f32 = 0.1;
5051 const MID_RANGE: f32 = 18.0;
5053
5054 enum FCounters {
5055 HealthThreshold = 0,
5056 }
5057 enum Timers {
5058 AttackChange = 0,
5059 }
5060 if agent.combat_state.timers[Timers::AttackChange as usize] > 2.5 {
5061 agent.combat_state.timers[Timers::AttackChange as usize] = 0.0;
5062 }
5063
5064 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5065 if !agent.combat_state.initialized {
5068 agent.combat_state.counters[FCounters::HealthThreshold as usize] = 1.0 - HP_THRESHOLD;
5069 agent.combat_state.initialized = true;
5070 } else if health_fraction < agent.combat_state.counters[FCounters::HealthThreshold as usize]
5071 {
5072 controller.push_basic_input(InputKind::Ability(0));
5074
5075 if matches!(
5076 self.char_state.ability_info().map(|ai| ai.input),
5077 Some(InputKind::Ability(0))
5078 ) && matches!(self.char_state.stage_section(), Some(StageSection::Recover))
5079 {
5080 agent.combat_state.counters[FCounters::HealthThreshold as usize] -= HP_THRESHOLD;
5081 }
5082 } else if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5083 {
5084 controller.push_basic_input(InputKind::Ability(0));
5086 } else if attack_data.dist_sqrd > LONG_RANGE.powi(2) {
5087 controller.push_basic_input(InputKind::Ability(1));
5089 } else if attack_data.dist_sqrd > MID_RANGE.powi(2) {
5090 controller.push_basic_input(InputKind::Secondary);
5092 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5093 if attack_data.angle < 60.0 {
5094 controller.push_basic_input(InputKind::Primary);
5096 } else if attack_data.angle > 60.0 {
5097 controller.push_basic_input(InputKind::Ability(0));
5099 }
5100 }
5101
5102 self.path_toward_target(
5104 agent,
5105 controller,
5106 tgt_data.pos.0,
5107 read_data,
5108 Path::AtTarget,
5109 (attack_data.dist_sqrd < (attack_data.min_attack_dist + MELEE_RANGE / 2.0).powi(2))
5110 .then_some(0.1),
5111 );
5112 }
5113
5114 pub fn handle_grave_warden_attack(
5115 &self,
5116 agent: &mut Agent,
5117 controller: &mut Controller,
5118 attack_data: &AttackData,
5119 tgt_data: &TargetData,
5120 read_data: &ReadData,
5121 ) {
5122 const GOLEM_MELEE_RANGE: f32 = 4.0;
5123 const GOLEM_LASER_RANGE: f32 = 30.0;
5124 const GOLEM_LONG_RANGE: f32 = 50.0;
5125 const GOLEM_TARGET_SPEED: f32 = 8.0;
5126
5127 enum ActionStateFCounters {
5128 FCounterGlayGolemAttack = 0,
5129 }
5130
5131 let golem_melee_range = self.body.map_or(0.0, |b| b.max_radius()) + GOLEM_MELEE_RANGE;
5132 let health_fraction = self.health.map_or(1.0, |h| h.fraction());
5135 let target_speed_cross_sqd = agent
5137 .target
5138 .as_ref()
5139 .map(|t| t.target)
5140 .and_then(|e| read_data.velocities.get(e))
5141 .map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared());
5142 let line_of_sight_with_target = || {
5143 entities_have_line_of_sight(
5144 self.pos,
5145 self.body,
5146 self.scale,
5147 tgt_data.pos,
5148 tgt_data.body,
5149 tgt_data.scale,
5150 read_data,
5151 )
5152 };
5153
5154 if attack_data.dist_sqrd < golem_melee_range.powi(2) {
5155 if agent.combat_state.counters[ActionStateFCounters::FCounterGlayGolemAttack as usize]
5156 < 7.5
5157 {
5158 controller.push_basic_input(InputKind::Primary);
5160 agent.combat_state.counters
5161 [ActionStateFCounters::FCounterGlayGolemAttack as usize] += read_data.dt.0;
5162 } else {
5163 controller.push_basic_input(InputKind::Ability(1));
5165 if matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5166 {
5167 agent.combat_state.counters
5168 [ActionStateFCounters::FCounterGlayGolemAttack as usize] = 0.0;
5169 }
5170 }
5171 } else if attack_data.dist_sqrd < GOLEM_LASER_RANGE.powi(2) {
5172 if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(5))
5173 || target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2)
5174 && line_of_sight_with_target()
5175 && attack_data.angle < 45.0
5176 {
5177 controller.push_basic_input(InputKind::Secondary);
5180 } else if health_fraction < 0.7 {
5181 controller.push_basic_input(InputKind::Ability(0));
5184 }
5185 } else if attack_data.dist_sqrd < GOLEM_LONG_RANGE.powi(2) {
5186 if target_speed_cross_sqd < GOLEM_TARGET_SPEED.powi(2) && line_of_sight_with_target() {
5187 controller.push_basic_input(InputKind::Ability(1));
5189 } else if health_fraction < 0.7 {
5190 controller.push_basic_input(InputKind::Ability(0));
5193 }
5194 }
5195
5196 self.path_toward_target(
5198 agent,
5199 controller,
5200 tgt_data.pos.0,
5201 read_data,
5202 Path::Separate,
5203 (attack_data.dist_sqrd
5204 < (attack_data.min_attack_dist + GOLEM_MELEE_RANGE / 1.5).powi(2))
5205 .then_some(0.1),
5206 );
5207 }
5208
5209 pub fn handle_tidal_warrior_attack(
5210 &self,
5211 agent: &mut Agent,
5212 controller: &mut Controller,
5213 attack_data: &AttackData,
5214 tgt_data: &TargetData,
5215 read_data: &ReadData,
5216 ) {
5217 const SCUTTLE_RANGE: f32 = 40.0;
5218 const BUBBLE_RANGE: f32 = 20.0;
5219 const MINION_SUMMON_THRESHOLD: f32 = 0.20;
5220
5221 enum ActionStateConditions {
5222 ConditionCounterInitialized = 0,
5223 }
5224
5225 enum ActionStateFCounters {
5226 FCounterMinionSummonThreshold = 0,
5227 }
5228
5229 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5230 let line_of_sight_with_target = || {
5231 entities_have_line_of_sight(
5232 self.pos,
5233 self.body,
5234 self.scale,
5235 tgt_data.pos,
5236 tgt_data.body,
5237 tgt_data.scale,
5238 read_data,
5239 )
5240 };
5241 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
5242 if !agent.combat_state.conditions
5245 [ActionStateConditions::ConditionCounterInitialized as usize]
5246 {
5247 agent.combat_state.counters
5248 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5249 1.0 - MINION_SUMMON_THRESHOLD;
5250 agent.combat_state.conditions
5251 [ActionStateConditions::ConditionCounterInitialized as usize] = true;
5252 }
5253
5254 if agent.combat_state.counters[ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5255 > health_fraction
5256 {
5257 controller.push_basic_input(InputKind::Ability(1));
5259
5260 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5261 {
5262 agent.combat_state.counters
5263 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5264 MINION_SUMMON_THRESHOLD;
5265 }
5266 } else if attack_data.dist_sqrd < SCUTTLE_RANGE.powi(2) {
5267 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5268 {
5269 controller.push_basic_input(InputKind::Secondary);
5271 } else if attack_data.dist_sqrd < BUBBLE_RANGE.powi(2) {
5272 if matches!(self.char_state, CharacterState::BasicBeam(c) if !matches!(c.stage_section, StageSection::Recover) && c.timer < Duration::from_secs(10))
5273 {
5274 controller.push_basic_input(InputKind::Ability(0));
5277 } else if attack_data.in_min_range() && attack_data.angle < 60.0 {
5278 controller.push_basic_input(InputKind::Primary);
5280 } else if attack_data.angle < 30.0 && line_of_sight_with_target() {
5281 controller.push_basic_input(InputKind::Ability(0));
5284 }
5285 } else if attack_data.angle < 90.0 && line_of_sight_with_target() {
5286 controller.push_basic_input(InputKind::Secondary);
5289 }
5290 }
5291 let path = if tgt_data.pos.0.z < self.pos.0.z {
5292 home
5293 } else {
5294 tgt_data.pos.0
5295 };
5296 self.path_toward_target(agent, controller, path, read_data, Path::AtTarget, None);
5299 }
5300
5301 pub fn handle_yeti_attack(
5302 &self,
5303 agent: &mut Agent,
5304 controller: &mut Controller,
5305 attack_data: &AttackData,
5306 tgt_data: &TargetData,
5307 read_data: &ReadData,
5308 ) {
5309 const ICE_SPIKES_RANGE: f32 = 15.0;
5310 const ICE_BREATH_RANGE: f32 = 10.0;
5311 const ICE_BREATH_TIMER: f32 = 10.0;
5312 const SNOWBALL_MAX_RANGE: f32 = 50.0;
5313
5314 enum ActionStateFCounters {
5315 FCounterYetiAttack = 0,
5316 }
5317
5318 agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize] +=
5319 read_data.dt.0;
5320
5321 if attack_data.dist_sqrd < ICE_BREATH_RANGE.powi(2) {
5322 if matches!(self.char_state, CharacterState::BasicBeam(c) if c.timer < Duration::from_secs(2))
5323 {
5324 controller.push_basic_input(InputKind::Ability(0));
5326 } else if agent.combat_state.counters[ActionStateFCounters::FCounterYetiAttack as usize]
5327 > ICE_BREATH_TIMER
5328 {
5329 controller.push_basic_input(InputKind::Ability(0));
5331
5332 if matches!(self.char_state, CharacterState::BasicBeam(_)) {
5333 agent.combat_state.counters
5335 [ActionStateFCounters::FCounterYetiAttack as usize] = 0.0;
5336 }
5337 } else if attack_data.in_min_range() {
5338 controller.push_basic_input(InputKind::Primary);
5340 } else {
5341 controller.push_basic_input(InputKind::Secondary);
5343 }
5344 } else if attack_data.dist_sqrd < ICE_SPIKES_RANGE.powi(2) && attack_data.angle < 60.0 {
5345 controller.push_basic_input(InputKind::Secondary);
5347 } else if attack_data.dist_sqrd < SNOWBALL_MAX_RANGE.powi(2) && attack_data.angle < 60.0 {
5348 controller.push_basic_input(InputKind::Ability(1));
5350 }
5351
5352 self.path_toward_target(
5354 agent,
5355 controller,
5356 tgt_data.pos.0,
5357 read_data,
5358 Path::AtTarget,
5359 attack_data.in_min_range().then_some(0.1),
5360 );
5361 }
5362
5363 pub fn handle_elephant_attack(
5364 &self,
5365 agent: &mut Agent,
5366 controller: &mut Controller,
5367 attack_data: &AttackData,
5368 tgt_data: &TargetData,
5369 read_data: &ReadData,
5370 rng: &mut impl RngExt,
5371 ) {
5372 const MELEE_RANGE: f32 = 10.0;
5373 const RANGED_RANGE: f32 = 20.0;
5374 const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
5375 desired_energy: 30.0,
5376 combo_scaling_buildup: 0,
5377 };
5378
5379 const GOUGE: InputKind = InputKind::Primary;
5380 const DASH: InputKind = InputKind::Secondary;
5381 const STOMP: InputKind = InputKind::Ability(0);
5382 const WATER: InputKind = InputKind::Ability(1);
5383 const VACUUM: InputKind = InputKind::Ability(2);
5384
5385 let could_use = |input| {
5386 Option::<AbilityInput>::from(input)
5387 .and_then(|ability_input| self.extract_ability(ability_input))
5388 .is_some_and(|ability_data| {
5389 ability_data.could_use(
5390 attack_data,
5391 self,
5392 tgt_data,
5393 read_data,
5394 ABILITY_PREFERENCES,
5395 )
5396 })
5397 };
5398
5399 let dashing = matches!(self.char_state, CharacterState::DashMelee(_))
5400 && self.char_state.stage_section() != Some(StageSection::Recover);
5401
5402 if dashing {
5403 controller.push_basic_input(DASH);
5404 } else if rng.random_bool(0.05) {
5405 if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5406 if rng.random_bool(0.5) && could_use(STOMP) {
5407 controller.push_basic_input(STOMP);
5408 } else {
5409 controller.push_basic_input(GOUGE);
5410 }
5411 } else if attack_data.dist_sqrd < RANGED_RANGE.powi(2) {
5412 if rng.random_bool(0.5) {
5413 controller.push_basic_input(WATER);
5414 } else if could_use(VACUUM) {
5415 controller.push_basic_input(VACUUM);
5416 } else {
5417 controller.push_basic_input(DASH);
5418 }
5419 } else {
5420 controller.push_basic_input(DASH);
5421 }
5422 }
5423
5424 self.path_toward_target(
5425 agent,
5426 controller,
5427 tgt_data.pos.0,
5428 read_data,
5429 Path::AtTarget,
5430 None,
5431 );
5432 }
5433
5434 pub fn handle_rocksnapper_attack(
5435 &self,
5436 agent: &mut Agent,
5437 controller: &mut Controller,
5438 attack_data: &AttackData,
5439 tgt_data: &TargetData,
5440 read_data: &ReadData,
5441 ) {
5442 const LEAP_TIMER: f32 = 3.0;
5443 const DASH_TIMER: f32 = 5.0;
5444 const LEAP_RANGE: f32 = 20.0;
5445 const MELEE_RANGE: f32 = 5.0;
5446
5447 enum ActionStateTimers {
5448 TimerRocksnapperDash = 0,
5449 TimerRocksnapperLeap = 1,
5450 }
5451 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] +=
5452 read_data.dt.0;
5453 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] +=
5454 read_data.dt.0;
5455
5456 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5457 {
5458 controller.push_basic_input(InputKind::Secondary);
5460 } else if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize]
5461 > DASH_TIMER
5462 {
5463 controller.push_basic_input(InputKind::Secondary);
5465
5466 if matches!(self.char_state, CharacterState::DashMelee(_)) {
5467 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] = 0.0;
5469 }
5470 } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) && attack_data.angle < 90.0 {
5471 if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize]
5472 > LEAP_TIMER
5473 {
5474 controller.push_basic_input(InputKind::Ability(0));
5476
5477 if matches!(self.char_state, CharacterState::LeapShockwave(_)) {
5478 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] =
5480 0.0;
5481 }
5482 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5483 controller.push_basic_input(InputKind::Primary);
5485 }
5486 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5487 controller.push_basic_input(InputKind::Primary);
5489 }
5490
5491 self.path_toward_target(
5493 agent,
5494 controller,
5495 tgt_data.pos.0,
5496 read_data,
5497 Path::AtTarget,
5498 None,
5499 );
5500 }
5501
5502 pub fn handle_roshwalr_attack(
5503 &self,
5504 agent: &mut Agent,
5505 controller: &mut Controller,
5506 attack_data: &AttackData,
5507 tgt_data: &TargetData,
5508 read_data: &ReadData,
5509 ) {
5510 const SLOW_CHARGE_RANGE: f32 = 12.5;
5511 const SHOCKWAVE_RANGE: f32 = 12.5;
5512 const SHOCKWAVE_TIMER: f32 = 15.0;
5513 const MELEE_RANGE: f32 = 4.0;
5514
5515 enum ActionStateFCounters {
5516 FCounterRoshwalrAttack = 0,
5517 }
5518
5519 agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] +=
5520 read_data.dt.0;
5521 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5522 {
5523 controller.push_basic_input(InputKind::Ability(0));
5525 } else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 270.0 {
5526 if agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize]
5527 > SHOCKWAVE_TIMER
5528 {
5529 controller.push_basic_input(InputKind::Ability(0));
5531
5532 if matches!(self.char_state, CharacterState::Shockwave(_)) {
5533 agent.combat_state.counters
5535 [ActionStateFCounters::FCounterRoshwalrAttack as usize] = 0.0;
5536 }
5537 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5538 controller.push_basic_input(InputKind::Primary);
5540 }
5541 } else if attack_data.dist_sqrd > SLOW_CHARGE_RANGE.powi(2) {
5542 controller.push_basic_input(InputKind::Secondary);
5544 }
5545
5546 self.path_toward_target(
5548 agent,
5549 controller,
5550 tgt_data.pos.0,
5551 read_data,
5552 Path::AtTarget,
5553 None,
5554 );
5555 }
5556
5557 pub fn handle_harvester_attack(
5558 &self,
5559 agent: &mut Agent,
5560 controller: &mut Controller,
5561 attack_data: &AttackData,
5562 tgt_data: &TargetData,
5563 read_data: &ReadData,
5564 rng: &mut impl RngExt,
5565 ) {
5566 const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
5580 const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
5581 const PATH_RANGE_FACTOR: f32 = 0.4; const SCYTHE_RANGE_FACTOR: f32 = 0.75; const SCYTHE_AIM_FACTOR: f32 = 0.7;
5584 const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
5585 const FIREBREATH_AIM_FACTOR: f32 = 0.8;
5586 const FIREBREATH_TIME_LIMIT: f32 = 4.0;
5587 const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; const FIREBREATH_COOLDOWN: f32 = 3.5;
5589 const PUMPKIN_RANGE_FACTOR: f32 = 0.75;
5590 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;
5596 const HAS_SUMMONED_SECOND_VINES: usize = 1;
5597 const FIREBREATH: usize = 0;
5599 const MIXUP: usize = 1;
5600 const FAR_PUMPKIN: usize = 2;
5601 const CLOSE_MIXUP_COOLDOWN: usize = 0;
5603 const MID_MIXUP_COOLDOWN: usize = 1;
5604 const FAR_PUMPKIN_COOLDOWN: usize = 2;
5605
5606 let line_of_sight_with_target = || {
5608 entities_have_line_of_sight(
5609 self.pos,
5610 self.body,
5611 self.scale,
5612 tgt_data.pos,
5613 tgt_data.body,
5614 tgt_data.scale,
5615 read_data,
5616 )
5617 };
5618
5619 let (scythe_range, scythe_angle) = {
5622 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
5623 self.extract_ability(AbilityInput::Primary)
5624 {
5625 (range, angle)
5626 } else {
5627 (0.0, 0.0)
5628 }
5629 };
5630 let (firebreath_range, firebreath_angle) = {
5631 if let Some(AbilityData::BasicBeam { range, angle, .. }) =
5632 self.extract_ability(AbilityInput::Secondary)
5633 {
5634 (range, angle)
5635 } else {
5636 (0.0, 0.0)
5637 }
5638 };
5639 let pumpkin_speed = {
5640 if let Some(AbilityData::BasicRanged {
5641 projectile_speed, ..
5642 }) = self.extract_ability(AbilityInput::Auxiliary(0))
5643 {
5644 projectile_speed
5645 } else {
5646 0.0
5647 }
5648 };
5649 let pumpkin_max_range =
5651 projectile_flat_range(pumpkin_speed, self.body.map_or(0.0, |b| b.height()));
5652
5653 let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
5655 let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
5656 let is_in_summon_recovery = matches!(self.char_state, CharacterState::SpriteSummon(data) if matches!(data.stage_section, StageSection::Recover));
5657 let firebreath_timer = if let CharacterState::BasicBeam(data) = self.char_state {
5658 data.timer
5659 } else {
5660 Default::default()
5661 };
5662 let is_using_mixup = is_using_firebreath || is_using_pumpkin;
5663
5664 if !agent.combat_state.initialized {
5666 agent.combat_state.initialized = true;
5667 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5668 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5669 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5670 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5671 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5672 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5673 }
5674
5675 if is_in_summon_recovery {
5679 agent.combat_state.timers[FIREBREATH] = 0.0;
5681 agent.combat_state.timers[MIXUP] = 0.0;
5682 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5683 } else {
5684 if is_using_firebreath {
5686 agent.combat_state.timers[FIREBREATH] = 0.0;
5687 } else {
5688 agent.combat_state.timers[FIREBREATH] += read_data.dt.0;
5689 }
5690 if is_using_mixup {
5691 agent.combat_state.timers[MIXUP] = 0.0;
5692 } else {
5693 agent.combat_state.timers[MIXUP] += read_data.dt.0;
5694 }
5695 if is_using_pumpkin {
5696 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5697 } else {
5698 agent.combat_state.timers[FAR_PUMPKIN] += read_data.dt.0;
5699 }
5700 }
5701
5702 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5704 if health_fraction < SECOND_VINE_CREATION_THRESHOLD
5706 && !agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES]
5707 {
5708 controller.push_basic_input(InputKind::Ability(2));
5710 if is_in_summon_recovery {
5712 agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES] = true;
5713 }
5714 }
5715 else if health_fraction < FIRST_VINE_CREATION_THRESHOLD
5717 && !agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES]
5718 {
5719 controller.push_basic_input(InputKind::Ability(1));
5721 if is_in_summon_recovery {
5723 agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES] = true;
5724 }
5725 }
5726 else if attack_data.dist_sqrd
5728 < (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
5729 {
5730 if is_using_firebreath
5732 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
5733 {
5734 controller.push_basic_input(InputKind::Secondary);
5735 }
5736 if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
5738 if agent.combat_state.timers[MIXUP]
5740 > agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN]
5741 {
5743 if agent.combat_state.timers[FIREBREATH] < FIREBREATH_COOLDOWN {
5745 controller.push_basic_input(InputKind::Ability(0));
5746 }
5747 else if rng.random_bool(0.5) {
5749 controller.push_basic_input(InputKind::Secondary);
5750 } else {
5751 controller.push_basic_input(InputKind::Ability(0));
5752 }
5753 if is_using_mixup {
5755 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5756 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5757 }
5758 }
5759 else {
5761 controller.push_basic_input(InputKind::Primary);
5762 }
5763 }
5764 } else if attack_data.dist_sqrd < firebreath_range.powi(2) {
5766 #[expect(clippy::if_same_then_else)]
5768 if is_using_firebreath
5769 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
5770 {
5771 controller.push_basic_input(InputKind::Secondary);
5772 }
5773 else if attack_data.dist_sqrd < (firebreath_range * FIREBREATH_RANGE_FACTOR).powi(2)
5775 && attack_data.angle < firebreath_angle * FIREBREATH_AIM_FACTOR
5776 && agent.combat_state.timers[FIREBREATH] > FIREBREATH_COOLDOWN
5777 {
5778 controller.push_basic_input(InputKind::Secondary);
5779 }
5780 else if agent.combat_state.timers[MIXUP]
5782 > agent.combat_state.counters[MID_MIXUP_COOLDOWN]
5783 {
5784 controller.push_basic_input(InputKind::Ability(0));
5785 if is_using_pumpkin {
5787 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5788 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5789 }
5790 }
5791 }
5792 else if attack_data.dist_sqrd < (pumpkin_max_range * PUMPKIN_RANGE_FACTOR).powi(2)
5794 && agent.combat_state.timers[FAR_PUMPKIN]
5795 > agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN]
5796 && line_of_sight_with_target()
5797 {
5798 controller.push_basic_input(InputKind::Ability(0));
5800 if is_using_pumpkin {
5802 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5803 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5804 }
5805 }
5806
5807 if attack_data.dist_sqrd
5810 > (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
5811 {
5812 self.path_toward_target(
5813 agent,
5814 controller,
5815 tgt_data.pos.0,
5816 read_data,
5817 Path::AtTarget,
5818 None,
5819 );
5820 }
5821 else if attack_data.angle > 0.0 {
5823 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
5825 .xy()
5826 .try_normalized()
5827 .unwrap_or_else(Vec2::zero)
5828 * 0.001; }
5830 }
5831
5832 pub fn handle_frostgigas_attack(
5833 &self,
5834 agent: &mut Agent,
5835 controller: &mut Controller,
5836 attack_data: &AttackData,
5837 tgt_data: &TargetData,
5838 read_data: &ReadData,
5839 rng: &mut impl RngExt,
5840 ) {
5841 const GIGAS_MELEE_RANGE: f32 = 12.0;
5842 const GIGAS_SPIKE_RANGE: f32 = 16.0;
5843 const ICEBOMB_RANGE: f32 = 70.0;
5844 const GIGAS_LEAP_RANGE: f32 = 50.0;
5845 const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
5846 const FLASHFREEZE_RANGE: f32 = 30.;
5847
5848 enum ActionStateTimers {
5849 AttackChange,
5850 Bonk,
5851 }
5852
5853 enum ActionStateFCounters {
5854 FCounterMinionSummonThreshold = 0,
5855 }
5856
5857 enum ActionStateICounters {
5858 CurrentAbility = 0,
5863 }
5864
5865 let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0);
5866 let remote_spikes_action = || ControlAction::StartInput {
5867 input: InputKind::Ability(5),
5868 target_entity: None,
5869 select_pos: Some(tgt_data.pos.0),
5870 };
5871
5872 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5873 if !agent.combat_state.initialized {
5876 agent.combat_state.counters
5877 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5878 1.0 - MINION_SUMMON_THRESHOLD;
5879 agent.combat_state.initialized = true;
5880 }
5881
5882 if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 {
5884 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
5885 } else {
5886 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
5887 }
5888 agent.combat_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0;
5889
5890 if health_fraction
5891 < agent.combat_state.counters
5892 [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5893 {
5894 controller.push_basic_input(InputKind::Ability(3));
5896
5897 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5898 {
5899 agent.combat_state.counters
5900 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5901 MINION_SUMMON_THRESHOLD;
5902 }
5903 } else if let Some(ability) = Some(
5905 &mut agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize],
5906 )
5907 .filter(|i| **i != 0)
5908 {
5909 if *ability == 3 && should_use_targeted_spikes() {
5910 *ability = 5
5911 };
5912
5913 let reset = match ability {
5914 1 => {
5916 controller.push_basic_input(InputKind::Ability(1));
5917 matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
5918 },
5919 2 => {
5921 controller.push_basic_input(InputKind::Ability(4));
5922 matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover))
5923 },
5924 3 => {
5926 controller.push_basic_input(InputKind::Ability(0));
5927 matches!(self.char_state, CharacterState::SpriteSummon(c)
5928 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner)))
5929 },
5930 4 => {
5932 controller.push_basic_input(InputKind::Ability(7));
5933 matches!(self.char_state, CharacterState::RapidMelee(c) if matches!(c.stage_section, StageSection::Recover))
5934 },
5935 5 => {
5937 controller.push_action(remote_spikes_action());
5938 matches!(self.char_state, CharacterState::SpriteSummon(c)
5939 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target)))
5940 },
5941 6 => {
5943 controller.push_basic_input(InputKind::Ability(2));
5944 matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5945 },
5946 _ => true,
5948 };
5949
5950 if reset {
5951 *ability = 0;
5952 }
5953 } else if attack_data.dist_sqrd > 5f32.powi(2)
5957 && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6
5959 && rng.random_bool((0.2 * read_data.dt.0).min(1.0) as f64)
5961 {
5962 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5963 rng.random_range(5..=6);
5964 } else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
5965 if agent.combat_state.timers[ActionStateTimers::Bonk as usize] > 10. {
5967 controller.push_basic_input(InputKind::Ability(6));
5968
5969 if matches!(self.char_state, CharacterState::BasicMelee(c)
5970 if matches!(c.stage_section, StageSection::Recover) &&
5971 c.static_data.ability_info.ability.is_some_and(|meta| matches!(meta.ability, Ability::MainWeaponAux(6)))
5972 ) {
5973 agent.combat_state.timers[ActionStateTimers::Bonk as usize] =
5974 rng.random_range(0.0..3.0);
5975 }
5976 } else if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 4.0
5978 && rng.random_bool(0.1 * read_data.dt.0.min(1.0) as f64)
5979 {
5980 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5981 rng.random_range(1..=4);
5982 } else if attack_data.angle > 90.0
5985 || agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 5.0
5986 {
5987 if attack_data.angle > 120.0 {
5989 agent.combat_state.int_counters
5990 [ActionStateICounters::CurrentAbility as usize] = 4;
5991 } else {
5992 controller.push_basic_input(InputKind::Secondary);
5993 }
5994 } else {
5995 controller.push_basic_input(InputKind::Primary);
5996 }
5997 } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
5998 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
5999 {
6000 if should_use_targeted_spikes() {
6001 controller.push_action(remote_spikes_action());
6002 } else {
6003 controller.push_basic_input(InputKind::Ability(0));
6004 }
6005 } else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
6006 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
6007 {
6008 controller.push_basic_input(InputKind::Ability(4));
6009 } else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2)
6011 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 3.0
6012 {
6013 controller.push_basic_input(InputKind::Ability(1));
6014 } else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2)
6015 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 3.0
6016 {
6017 controller.push_basic_input(InputKind::Ability(2));
6018 } else {
6020 controller.push_action(remote_spikes_action());
6021 }
6022
6023 self.path_toward_target(
6025 agent,
6026 controller,
6027 tgt_data.pos.0,
6028 read_data,
6029 Path::AtTarget,
6030 attack_data.in_min_range().then_some(0.1),
6031 );
6032 }
6033
6034 pub fn handle_boreal_hammer_attack(
6035 &self,
6036 agent: &mut Agent,
6037 controller: &mut Controller,
6038 attack_data: &AttackData,
6039 tgt_data: &TargetData,
6040 read_data: &ReadData,
6041 rng: &mut impl RngExt,
6042 ) {
6043 enum ActionStateTimers {
6044 TimerHandleHammerAttack = 0,
6045 }
6046
6047 let has_energy = |need| self.energy.current() > need;
6048
6049 let use_leap = |controller: &mut Controller| {
6050 controller.push_basic_input(InputKind::Ability(0));
6051 };
6052
6053 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] +=
6054 read_data.dt.0;
6055
6056 if attack_data.in_min_range() && attack_data.angle < 45.0 {
6057 controller.inputs.move_dir = Vec2::zero();
6058 if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0
6059 {
6060 controller.push_cancel_input(InputKind::Secondary);
6061 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] =
6062 0.0;
6063 } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize]
6064 > 3.0
6065 {
6066 controller.push_basic_input(InputKind::Secondary);
6067 } else if has_energy(50.0) && rng.random_bool(0.9) {
6068 use_leap(controller);
6069 } else {
6070 controller.push_basic_input(InputKind::Primary);
6071 }
6072 } else {
6073 self.path_toward_target(
6074 agent,
6075 controller,
6076 tgt_data.pos.0,
6077 read_data,
6078 Path::Separate,
6079 None,
6080 );
6081
6082 if attack_data.dist_sqrd < 32.0f32.powi(2)
6083 && entities_have_line_of_sight(
6084 self.pos,
6085 self.body,
6086 self.scale,
6087 tgt_data.pos,
6088 tgt_data.body,
6089 tgt_data.scale,
6090 read_data,
6091 )
6092 {
6093 if rng.random_bool(0.5) && has_energy(50.0) {
6094 use_leap(controller);
6095 } else if agent.combat_state.timers
6096 [ActionStateTimers::TimerHandleHammerAttack as usize]
6097 > 2.0
6098 {
6099 controller.push_basic_input(InputKind::Secondary);
6100 } else if agent.combat_state.timers
6101 [ActionStateTimers::TimerHandleHammerAttack as usize]
6102 > 4.0
6103 {
6104 controller.push_cancel_input(InputKind::Secondary);
6105 agent.combat_state.timers
6106 [ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0;
6107 }
6108 }
6109 }
6110 }
6111
6112 pub fn handle_boreal_bow_attack(
6113 &self,
6114 agent: &mut Agent,
6115 controller: &mut Controller,
6116 attack_data: &AttackData,
6117 tgt_data: &TargetData,
6118 read_data: &ReadData,
6119 rng: &mut impl RngExt,
6120 ) {
6121 let line_of_sight_with_target = || {
6122 entities_have_line_of_sight(
6123 self.pos,
6124 self.body,
6125 self.scale,
6126 tgt_data.pos,
6127 tgt_data.body,
6128 tgt_data.scale,
6129 read_data,
6130 )
6131 };
6132
6133 let has_energy = |need| self.energy.current() > need;
6134
6135 let use_trap = |controller: &mut Controller| {
6136 controller.push_basic_input(InputKind::Ability(0));
6137 };
6138
6139 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6140 if rng.random_bool(0.5) && has_energy(15.0) {
6141 controller.push_basic_input(InputKind::Secondary);
6142 } else if attack_data.angle < 15.0 {
6143 controller.push_basic_input(InputKind::Primary);
6144 }
6145 } else if attack_data.dist_sqrd < (4.0 * attack_data.min_attack_dist).powi(2)
6146 && line_of_sight_with_target()
6147 {
6148 if rng.random_bool(0.5) && has_energy(15.0) {
6149 controller.push_basic_input(InputKind::Secondary);
6150 } else if has_energy(20.0) {
6151 use_trap(controller);
6152 }
6153 }
6154
6155 if has_energy(50.0) {
6156 if attack_data.dist_sqrd < (10.0 * attack_data.min_attack_dist).powi(2) {
6157 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6159 &*read_data.terrain,
6160 self.pos.0,
6161 self.vel.0,
6162 tgt_data.pos.0,
6163 TraversalConfig {
6164 min_tgt_dist: 1.25,
6165 ..self.traversal_config
6166 },
6167 &read_data.time,
6168 ) {
6169 self.unstuck_if(stuck, controller);
6170 if line_of_sight_with_target() && attack_data.angle < 45.0 {
6171 controller.inputs.move_dir = bearing
6172 .xy()
6173 .rotated_z(rng.random_range(0.5..1.57))
6174 .try_normalized()
6175 .unwrap_or_else(Vec2::zero)
6176 * 2.0
6177 * speed;
6178 } else {
6179 controller.inputs.move_dir =
6181 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6182 self.jump_if(bearing.z > 1.5, controller);
6183 controller.inputs.move_z = bearing.z;
6184 }
6185 }
6186 } else {
6187 self.path_toward_target(
6189 agent,
6190 controller,
6191 tgt_data.pos.0,
6192 read_data,
6193 Path::AtTarget,
6194 None,
6195 );
6196 }
6197 } else {
6198 self.path_toward_target(
6200 agent,
6201 controller,
6202 tgt_data.pos.0,
6203 read_data,
6204 Path::AtTarget,
6205 None,
6206 );
6207 }
6208 }
6209
6210 pub fn handle_firegigas_attack(
6211 &self,
6212 agent: &mut Agent,
6213 controller: &mut Controller,
6214 attack_data: &AttackData,
6215 tgt_data: &TargetData,
6216 read_data: &ReadData,
6217 rng: &mut impl RngExt,
6218 ) {
6219 const MELEE_RANGE: f32 = 12.0;
6220 const RANGED_RANGE: f32 = 27.0;
6221 const LEAP_RANGE: f32 = 50.0;
6222 const MINION_SUMMON_THRESHOLD: f32 = 1.0 / 8.0;
6223 const OVERHEAT_DUR: f32 = 3.0;
6224 const FORCE_GAP_CLOSER_TIMEOUT: f32 = 10.0;
6225
6226 enum ActionStateTimers {
6227 Special,
6228 Overheat,
6229 OutOfMeleeRange,
6230 }
6231
6232 enum ActionStateFCounters {
6233 FCounterMinionSummonThreshold,
6234 }
6235
6236 enum ActionStateConditions {
6237 VerticalStrikeCombo,
6238 WhirlwindTwice,
6239 }
6240
6241 const FAST_SLASH: InputKind = InputKind::Primary;
6242 const FAST_THRUST: InputKind = InputKind::Secondary;
6243 const SLOW_SLASH: InputKind = InputKind::Ability(0);
6244 const SLOW_THRUST: InputKind = InputKind::Ability(1);
6245 const LAVA_LEAP: InputKind = InputKind::Ability(2);
6246 const VERTICAL_STRIKE: InputKind = InputKind::Ability(3);
6247 const OVERHEAT: InputKind = InputKind::Ability(4);
6248 const WHIRLWIND: InputKind = InputKind::Ability(5);
6249 const EXPLOSIVE_STRIKE: InputKind = InputKind::Ability(6);
6250 const FIRE_PILLARS: InputKind = InputKind::Ability(7);
6251 const TARGETED_FIRE_PILLAR: InputKind = InputKind::Ability(8);
6252 const ASHEN_SUMMONS: InputKind = InputKind::Ability(9);
6253 const PARRY_PUNISH: InputKind = InputKind::Ability(10);
6254
6255 fn choose_weighted<const N: usize>(
6256 rng: &mut impl RngExt,
6257 choices: [(InputKind, f32); N],
6258 ) -> InputKind {
6259 choices
6260 .choose_weighted(rng, |(_, weight)| *weight)
6261 .expect("weights should be valid")
6262 .0
6263 }
6264
6265 fn rand_basic(rng: &mut impl RngExt, damage_fraction: f32) -> InputKind {
6267 choose_weighted(rng, [
6268 (FAST_SLASH, 2.0),
6269 (FAST_THRUST, 2.0),
6270 (SLOW_SLASH, 1.0 + damage_fraction),
6271 (SLOW_THRUST, 1.0 + damage_fraction),
6272 ])
6273 }
6274
6275 fn rand_special(rng: &mut impl RngExt) -> InputKind {
6277 choose_weighted(rng, [
6278 (WHIRLWIND, 6.0),
6279 (VERTICAL_STRIKE, 6.0),
6280 (OVERHEAT, 6.0),
6281 (EXPLOSIVE_STRIKE, 1.0),
6282 (LAVA_LEAP, 1.0),
6283 (FIRE_PILLARS, 1.0),
6284 ])
6285 }
6286
6287 fn rand_aoe(rng: &mut impl RngExt) -> InputKind {
6289 choose_weighted(rng, [
6290 (EXPLOSIVE_STRIKE, 1.0),
6291 (FIRE_PILLARS, 1.0),
6292 (WHIRLWIND, 2.0),
6293 ])
6294 }
6295
6296 fn rand_ranged(rng: &mut impl RngExt) -> InputKind {
6298 choose_weighted(rng, [
6299 (EXPLOSIVE_STRIKE, 1.0),
6300 (FIRE_PILLARS, 1.0),
6301 (OVERHEAT, 1.0),
6302 ])
6303 }
6304
6305 let cast_targeted_fire_pillar = |c: &mut Controller| {
6306 c.push_action(ControlAction::StartInput {
6307 input: TARGETED_FIRE_PILLAR,
6308 target_entity: tgt_data.uid,
6309 select_pos: None,
6310 })
6311 };
6312
6313 fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6314 !matches!(
6315 char_state,
6316 CharacterState::LeapMelee(_)
6317 | CharacterState::BasicMelee(_)
6318 | CharacterState::BasicBeam(_)
6319 | CharacterState::BasicSummon(_)
6320 | CharacterState::SpriteSummon(_)
6321 )
6322 }
6323
6324 if !agent.combat_state.initialized {
6326 agent.combat_state.counters
6327 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
6328 1.0 - MINION_SUMMON_THRESHOLD;
6329 agent.combat_state.initialized = true;
6330 }
6331
6332 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
6333 let damage_fraction = 1.0 - health_fraction;
6334 let cheesed_from_above = !agent.combat_state.conditions
6338 [ActionStateConditions::VerticalStrikeCombo as usize]
6339 && attack_data.dist_sqrd > 5f32.powi(2)
6340 && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6;
6341 let cheesed_in_water = matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { kind: LiquidKind::Water, depth, .. }) if depth >= 2.0);
6343 let cheesed = cheesed_from_above || cheesed_in_water;
6344 let tgt_airborne = tgt_data
6345 .physics_state
6346 .is_some_and(|physics| physics.on_ground.is_none() && physics.in_liquid().is_none());
6347 let tgt_missed_parry = match tgt_data.char_state {
6348 Some(CharacterState::RiposteMelee(data)) => {
6349 matches!(data.stage_section, StageSection::Recover) && data.whiffed
6350 },
6351 Some(CharacterState::BasicBlock(data)) => {
6352 matches!(data.stage_section, StageSection::Recover)
6353 && !data.static_data.parry_window.recover
6354 && !data.is_parry
6355 },
6356 _ => false,
6357 };
6358 let casting_beam = matches!(self.char_state, CharacterState::BasicBeam(_))
6359 && self.char_state.stage_section() != Some(StageSection::Recover);
6360
6361 agent.combat_state.timers[ActionStateTimers::Special as usize] += read_data.dt.0;
6363 if casting_beam {
6364 agent.combat_state.timers[ActionStateTimers::Overheat as usize] += read_data.dt.0;
6365 } else {
6366 agent.combat_state.timers[ActionStateTimers::Overheat as usize] = 0.0;
6367 }
6368 if attack_data.dist_sqrd > MELEE_RANGE.powi(2) {
6369 agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] +=
6370 read_data.dt.0;
6371 } else {
6372 agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] = 0.0;
6373 }
6374
6375 if casting_beam
6377 && agent.combat_state.timers[ActionStateTimers::Overheat as usize] < OVERHEAT_DUR
6378 {
6379 controller.push_basic_input(OVERHEAT);
6380 controller.inputs.look_dir = self
6381 .ori
6382 .look_dir()
6383 .to_horizontal()
6384 .unwrap_or_else(|| self.ori.look_dir());
6385 } else if health_fraction
6386 < agent.combat_state.counters
6387 [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
6388 {
6389 controller.push_basic_input(ASHEN_SUMMONS);
6391
6392 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
6393 {
6394 agent.combat_state.counters
6395 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
6396 MINION_SUMMON_THRESHOLD;
6397 }
6398 } else if can_cast_new_ability(self.char_state) {
6399 if cheesed {
6400 cast_targeted_fire_pillar(controller);
6401 } else if agent.combat_state.conditions
6402 [ActionStateConditions::VerticalStrikeCombo as usize]
6403 {
6404 if tgt_airborne {
6406 controller.push_basic_input(FAST_THRUST);
6407 }
6408
6409 agent.combat_state.conditions
6410 [ActionStateConditions::VerticalStrikeCombo as usize] = false;
6411 } else if agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize]
6412 {
6413 controller.push_basic_input(WHIRLWIND);
6414 agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize] =
6415 false;
6416 } else if agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize]
6417 > FORCE_GAP_CLOSER_TIMEOUT
6418 {
6419 controller.push_basic_input(LAVA_LEAP);
6421 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
6422 if tgt_missed_parry {
6423 controller.push_basic_input(PARRY_PUNISH);
6424 agent.combat_state.conditions
6425 [ActionStateConditions::VerticalStrikeCombo as usize] = true;
6426 } else if agent.combat_state.timers[ActionStateTimers::Special as usize] > 10.0 {
6427 let rand_special = rand_special(rng);
6429 match rand_special {
6430 VERTICAL_STRIKE => {
6431 agent.combat_state.conditions
6432 [ActionStateConditions::VerticalStrikeCombo as usize] = true
6433 },
6434 WHIRLWIND if rng.random_bool(0.2) => {
6435 agent.combat_state.conditions
6436 [ActionStateConditions::WhirlwindTwice as usize] = true
6437 },
6438 _ => {},
6439 }
6440 controller.push_basic_input(rand_special);
6441
6442 agent.combat_state.timers[ActionStateTimers::Special as usize] =
6443 rng.random_range(0.0..3.0 + 5.0 * damage_fraction);
6444 } else if attack_data.angle > 90.0 {
6445 let rand_aoe = rand_aoe(rng);
6447 match rand_aoe {
6448 WHIRLWIND if rng.random_bool(0.2) => {
6449 agent.combat_state.conditions
6450 [ActionStateConditions::WhirlwindTwice as usize] = true
6451 },
6452 _ => {},
6453 }
6454
6455 controller.push_basic_input(rand_aoe);
6456 } else {
6457 controller.push_basic_input(rand_basic(rng, damage_fraction));
6459 }
6460 } else if attack_data.dist_sqrd < RANGED_RANGE.powi(2) {
6461 if rng.random_bool(0.05) {
6463 controller.push_basic_input(rand_ranged(rng));
6464 }
6465 } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) {
6466 controller.push_basic_input(LAVA_LEAP);
6468 } else if rng.random_bool(0.1) {
6469 cast_targeted_fire_pillar(controller);
6471 }
6472 }
6473
6474 self.path_toward_target(
6475 agent,
6476 controller,
6477 tgt_data.pos.0,
6478 read_data,
6479 Path::AtTarget,
6480 attack_data.in_min_range().then_some(0.1),
6481 );
6482
6483 if self.physics_state.in_liquid().is_some() {
6485 controller.push_basic_input(InputKind::Jump);
6486 }
6487 if self.physics_state.in_liquid().is_some() {
6488 controller.inputs.move_z = 1.0;
6489 }
6490 }
6491
6492 pub fn handle_ashen_axe_attack(
6493 &self,
6494 agent: &mut Agent,
6495 controller: &mut Controller,
6496 attack_data: &AttackData,
6497 tgt_data: &TargetData,
6498 read_data: &ReadData,
6499 rng: &mut impl RngExt,
6500 ) {
6501 const IMMOLATION_COOLDOWN: f32 = 50.0;
6502 const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6503 desired_energy: 30.0,
6504 combo_scaling_buildup: 0,
6505 };
6506
6507 enum ActionStateTimers {
6508 SinceSelfImmolation,
6509 }
6510
6511 const DOUBLE_STRIKE: InputKind = InputKind::Primary;
6512 const FLAME_WAVE: InputKind = InputKind::Secondary;
6513 const KNOCKBACK_COMBO: InputKind = InputKind::Ability(0);
6514 const SELF_IMMOLATION: InputKind = InputKind::Ability(1);
6515
6516 fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6517 !matches!(
6518 char_state,
6519 CharacterState::ComboMelee2(_)
6520 | CharacterState::Shockwave(_)
6521 | CharacterState::SelfBuff(_)
6522 )
6523 }
6524
6525 let could_use = |input| {
6526 Option::<AbilityInput>::from(input)
6527 .and_then(|ability_input| self.extract_ability(ability_input))
6528 .is_some_and(|ability_data| {
6529 ability_data.could_use(
6530 attack_data,
6531 self,
6532 tgt_data,
6533 read_data,
6534 ABILITY_PREFERENCES,
6535 )
6536 })
6537 };
6538
6539 if !agent.combat_state.initialized {
6541 agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6542 IMMOLATION_COOLDOWN;
6543 agent.combat_state.initialized = true;
6544 }
6545
6546 agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] +=
6547 read_data.dt.0;
6548
6549 if self
6550 .char_state
6551 .ability_info()
6552 .map(|ai| ai.input)
6553 .is_some_and(|input_kind| input_kind == KNOCKBACK_COMBO)
6554 {
6555 controller.push_basic_input(KNOCKBACK_COMBO);
6556 } else if can_cast_new_ability(self.char_state)
6557 && agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize]
6558 >= IMMOLATION_COOLDOWN
6559 && could_use(SELF_IMMOLATION)
6560 {
6561 agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6562 rng.random_range(0.0..5.0);
6563
6564 controller.push_basic_input(SELF_IMMOLATION);
6565 } else if rng.random_bool(0.35) && could_use(KNOCKBACK_COMBO) {
6566 controller.push_basic_input(KNOCKBACK_COMBO);
6567 } else if could_use(DOUBLE_STRIKE) {
6568 controller.push_basic_input(DOUBLE_STRIKE);
6569 } else if rng.random_bool(0.2) && could_use(FLAME_WAVE) {
6570 controller.push_basic_input(FLAME_WAVE);
6571 }
6572
6573 self.path_toward_target(
6574 agent,
6575 controller,
6576 tgt_data.pos.0,
6577 read_data,
6578 Path::AtTarget,
6579 None,
6580 );
6581 }
6582
6583 pub fn handle_ashen_staff_attack(
6584 &self,
6585 agent: &mut Agent,
6586 controller: &mut Controller,
6587 attack_data: &AttackData,
6588 tgt_data: &TargetData,
6589 read_data: &ReadData,
6590 rng: &mut impl RngExt,
6591 ) {
6592 const ABILITY_COOLDOWN: f32 = 50.0;
6593 const INITIAL_COOLDOWN: f32 = ABILITY_COOLDOWN - 10.0;
6594 const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6595 desired_energy: 40.0,
6596 combo_scaling_buildup: 0,
6597 };
6598
6599 enum ActionStateTimers {
6600 SinceAbility,
6601 }
6602
6603 const FIREBALL: InputKind = InputKind::Primary;
6604 const FLAME_WALL: InputKind = InputKind::Ability(0);
6605 const SUMMON_CRUX: InputKind = InputKind::Ability(1);
6606
6607 fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6608 !matches!(
6609 char_state,
6610 CharacterState::BasicRanged(_)
6611 | CharacterState::BasicBeam(_)
6612 | CharacterState::RapidMelee(_)
6613 | CharacterState::BasicAura(_)
6614 )
6615 }
6616
6617 let could_use = |input| {
6618 Option::<AbilityInput>::from(input)
6619 .and_then(|ability_input| self.extract_ability(ability_input))
6620 .is_some_and(|ability_data| {
6621 ability_data.could_use(
6622 attack_data,
6623 self,
6624 tgt_data,
6625 read_data,
6626 ABILITY_PREFERENCES,
6627 )
6628 })
6629 };
6630
6631 if !agent.combat_state.initialized {
6633 agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] = INITIAL_COOLDOWN;
6634 agent.combat_state.initialized = true;
6635 }
6636
6637 agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] += read_data.dt.0;
6638
6639 if can_cast_new_ability(self.char_state)
6640 && agent.combat_state.timers[ActionStateTimers::SinceAbility as usize]
6641 >= ABILITY_COOLDOWN
6642 && (could_use(FLAME_WALL) || could_use(SUMMON_CRUX))
6643 {
6644 agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] =
6645 rng.random_range(0.0..5.0);
6646
6647 if could_use(FLAME_WALL) && (rng.random_bool(0.5) || !could_use(SUMMON_CRUX)) {
6648 controller.push_basic_input(FLAME_WALL);
6649 } else {
6650 controller.push_basic_input(SUMMON_CRUX);
6651 }
6652 } else if rng.random_bool(0.5) && could_use(FIREBALL) {
6653 controller.push_basic_input(FIREBALL);
6654 }
6655
6656 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6657 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6659 &*read_data.terrain,
6660 self.pos.0,
6661 self.vel.0,
6662 tgt_data.pos.0,
6663 TraversalConfig {
6664 min_tgt_dist: 1.25,
6665 ..self.traversal_config
6666 },
6667 &read_data.time,
6668 ) {
6669 self.unstuck_if(stuck, controller);
6670 controller.inputs.move_dir =
6671 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6672 }
6673 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6674 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6676 &*read_data.terrain,
6677 self.pos.0,
6678 self.vel.0,
6679 tgt_data.pos.0,
6680 TraversalConfig {
6681 min_tgt_dist: 1.25,
6682 ..self.traversal_config
6683 },
6684 &read_data.time,
6685 ) {
6686 self.unstuck_if(stuck, controller);
6687 if entities_have_line_of_sight(
6688 self.pos,
6689 self.body,
6690 self.scale,
6691 tgt_data.pos,
6692 tgt_data.body,
6693 tgt_data.scale,
6694 read_data,
6695 ) && attack_data.angle < 45.0
6696 {
6697 controller.inputs.move_dir = bearing
6698 .xy()
6699 .rotated_z(rng.random_range(-1.57..-0.5))
6700 .try_normalized()
6701 .unwrap_or_else(Vec2::zero)
6702 * speed;
6703 } else {
6704 controller.inputs.move_dir =
6706 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6707 self.jump_if(bearing.z > 1.5, controller);
6708 controller.inputs.move_z = bearing.z;
6709 }
6710 }
6711 } else {
6712 self.path_toward_target(
6714 agent,
6715 controller,
6716 tgt_data.pos.0,
6717 read_data,
6718 Path::AtTarget,
6719 None,
6720 );
6721 }
6722 }
6723
6724 pub fn handle_cardinal_attack(
6725 &self,
6726 agent: &mut Agent,
6727 controller: &mut Controller,
6728 attack_data: &AttackData,
6729 tgt_data: &TargetData,
6730 read_data: &ReadData,
6731 rng: &mut impl RngExt,
6732 ) {
6733 const DESIRED_ENERGY_LEVEL: f32 = 50.0;
6734 const DESIRED_COMBO_LEVEL: u32 = 8;
6735 const MINION_SUMMON_THRESHOLD: f32 = 0.10;
6736
6737 enum ActionStateConditions {
6738 ConditionCounterInitialized = 0,
6739 }
6740
6741 enum ActionStateFCounters {
6742 FCounterHealthThreshold = 0,
6743 }
6744
6745 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
6746 if !agent.combat_state.conditions
6749 [ActionStateConditions::ConditionCounterInitialized as usize]
6750 {
6751 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6752 1.0 - MINION_SUMMON_THRESHOLD;
6753 agent.combat_state.conditions
6754 [ActionStateConditions::ConditionCounterInitialized as usize] = true;
6755 }
6756
6757 if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
6758 > health_fraction
6759 {
6760 controller.push_basic_input(InputKind::Ability(1));
6762
6763 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
6764 {
6765 agent.combat_state.counters
6766 [ActionStateFCounters::FCounterHealthThreshold as usize] -=
6767 MINION_SUMMON_THRESHOLD;
6768 }
6769 }
6770 else if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
6772 && entities_have_line_of_sight(
6773 self.pos,
6774 self.body,
6775 self.scale,
6776 tgt_data.pos,
6777 tgt_data.body,
6778 tgt_data.scale,
6779 read_data,
6780 )
6781 {
6782 if self.energy.current() > DESIRED_ENERGY_LEVEL
6785 && read_data
6786 .combos
6787 .get(*self.entity)
6788 .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
6789 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6790 buff.iter_kind(BuffKind::Regeneration)
6791 .peekable()
6792 .peek()
6793 .is_some()
6794 })
6795 {
6796 controller.push_basic_input(InputKind::Secondary);
6798 } else if self
6799 .skill_set
6800 .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
6801 && self.energy.current() > DESIRED_ENERGY_LEVEL
6802 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6803 buff.iter_kind(BuffKind::ProtectingWard)
6804 .peekable()
6805 .peek()
6806 .is_some()
6807 })
6808 {
6809 controller.push_basic_input(InputKind::Ability(0));
6812 } else {
6813 controller.push_basic_input(InputKind::Primary);
6816 }
6817 } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6818 if self.body.is_some_and(|b| b.is_humanoid())
6819 && self.energy.current()
6820 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
6821 && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
6822 {
6823 controller.push_basic_input(InputKind::Ability(0));
6825 } else if attack_data.angle < 15.0 {
6826 controller.push_basic_input(InputKind::Primary);
6827 }
6828 }
6829 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6832 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6834 &*read_data.terrain,
6835 self.pos.0,
6836 self.vel.0,
6837 tgt_data.pos.0,
6838 TraversalConfig {
6839 min_tgt_dist: 1.25,
6840 ..self.traversal_config
6841 },
6842 &read_data.time,
6843 ) {
6844 self.unstuck_if(stuck, controller);
6845 controller.inputs.move_dir =
6846 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6847 }
6848 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6849 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6851 &*read_data.terrain,
6852 self.pos.0,
6853 self.vel.0,
6854 tgt_data.pos.0,
6855 TraversalConfig {
6856 min_tgt_dist: 1.25,
6857 ..self.traversal_config
6858 },
6859 &read_data.time,
6860 ) {
6861 self.unstuck_if(stuck, controller);
6862 if entities_have_line_of_sight(
6863 self.pos,
6864 self.body,
6865 self.scale,
6866 tgt_data.pos,
6867 tgt_data.body,
6868 tgt_data.scale,
6869 read_data,
6870 ) && attack_data.angle < 45.0
6871 {
6872 controller.inputs.move_dir = bearing
6873 .xy()
6874 .rotated_z(rng.random_range(0.5..1.57))
6875 .try_normalized()
6876 .unwrap_or_else(Vec2::zero)
6877 * speed;
6878 } else {
6879 controller.inputs.move_dir =
6881 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6882 self.jump_if(bearing.z > 1.5, controller);
6883 controller.inputs.move_z = bearing.z;
6884 }
6885 }
6886 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
6888 && !matches!(self.char_state, CharacterState::BasicAura(_))
6889 && attack_data.dist_sqrd < 16.0f32.powi(2)
6890 && rng.random::<f32>() < 0.01
6891 {
6892 controller.push_basic_input(InputKind::Roll);
6893 }
6894 } else {
6895 self.path_toward_target(
6897 agent,
6898 controller,
6899 tgt_data.pos.0,
6900 read_data,
6901 Path::AtTarget,
6902 None,
6903 );
6904 }
6905 }
6906
6907 pub fn handle_sea_bishop_attack(
6908 &self,
6909 agent: &mut Agent,
6910 controller: &mut Controller,
6911 attack_data: &AttackData,
6912 tgt_data: &TargetData,
6913 read_data: &ReadData,
6914 rng: &mut impl RngExt,
6915 ) {
6916 let line_of_sight_with_target = || {
6917 entities_have_line_of_sight(
6918 self.pos,
6919 self.body,
6920 self.scale,
6921 tgt_data.pos,
6922 tgt_data.body,
6923 tgt_data.scale,
6924 read_data,
6925 )
6926 };
6927
6928 enum ActionStateTimers {
6929 TimerBeam = 0,
6930 }
6931 if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 6.0 {
6932 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
6933 } else {
6934 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
6935 }
6936
6937 if line_of_sight_with_target()
6939 && agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 3.0
6940 {
6941 controller.push_basic_input(InputKind::Primary);
6942 }
6943 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6946 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6948 &*read_data.terrain,
6949 self.pos.0,
6950 self.vel.0,
6951 tgt_data.pos.0,
6952 TraversalConfig {
6953 min_tgt_dist: 1.25,
6954 ..self.traversal_config
6955 },
6956 &read_data.time,
6957 ) {
6958 self.unstuck_if(stuck, controller);
6959 controller.inputs.move_dir =
6960 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6961 }
6962 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6963 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6965 &*read_data.terrain,
6966 self.pos.0,
6967 self.vel.0,
6968 tgt_data.pos.0,
6969 TraversalConfig {
6970 min_tgt_dist: 1.25,
6971 ..self.traversal_config
6972 },
6973 &read_data.time,
6974 ) {
6975 self.unstuck_if(stuck, controller);
6976 if line_of_sight_with_target() && attack_data.angle < 45.0 {
6977 controller.inputs.move_dir = bearing
6978 .xy()
6979 .rotated_z(rng.random_range(0.5..1.57))
6980 .try_normalized()
6981 .unwrap_or_else(Vec2::zero)
6982 * speed;
6983 } else {
6984 controller.inputs.move_dir =
6986 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6987 self.jump_if(bearing.z > 1.5, controller);
6988 controller.inputs.move_z = bearing.z;
6989 }
6990 }
6991 } else {
6992 self.path_toward_target(
6994 agent,
6995 controller,
6996 tgt_data.pos.0,
6997 read_data,
6998 Path::AtTarget,
6999 None,
7000 );
7001 }
7002 }
7003
7004 pub fn handle_cursekeeper_attack(
7005 &self,
7006 agent: &mut Agent,
7007 controller: &mut Controller,
7008 attack_data: &AttackData,
7009 tgt_data: &TargetData,
7010 read_data: &ReadData,
7011 rng: &mut impl RngExt,
7012 ) {
7013 enum ActionStateTimers {
7014 TimerBeam,
7015 TimerSummon,
7016 SelectSummon,
7017 }
7018 if tgt_data.pos.0.z - self.pos.0.z > 3.5 {
7019 controller.push_action(ControlAction::StartInput {
7020 input: InputKind::Ability(4),
7021 target_entity: agent
7022 .target
7023 .as_ref()
7024 .and_then(|t| read_data.uids.get(t.target))
7025 .copied(),
7026 select_pos: None,
7027 });
7028 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 12.0 {
7029 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
7030 } else {
7031 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
7032 }
7033
7034 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
7035 {
7036 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] = 0.0;
7037 agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] =
7038 rng.random_range(0..=3) as f32;
7039 } else {
7040 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] += read_data.dt.0;
7041 }
7042
7043 if agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] > 32.0 {
7044 match agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] as i32 {
7045 0 => controller.push_basic_input(InputKind::Ability(0)),
7046 1 => controller.push_basic_input(InputKind::Ability(1)),
7047 2 => controller.push_basic_input(InputKind::Ability(2)),
7048 _ => controller.push_basic_input(InputKind::Ability(3)),
7049 }
7050 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 6.0 {
7051 controller.push_basic_input(InputKind::Ability(5));
7052 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 9.0 {
7053 controller.push_basic_input(InputKind::Primary);
7054 } else {
7055 controller.push_basic_input(InputKind::Secondary);
7056 }
7057
7058 if attack_data.dist_sqrd > 10_f32.powi(2)
7059 || agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 4.0
7060 {
7061 self.path_toward_target(
7062 agent,
7063 controller,
7064 tgt_data.pos.0,
7065 read_data,
7066 Path::AtTarget,
7067 None,
7068 );
7069 }
7070 }
7071
7072 pub fn handle_shamanic_spirit_attack(
7073 &self,
7074 agent: &mut Agent,
7075 controller: &mut Controller,
7076 attack_data: &AttackData,
7077 tgt_data: &TargetData,
7078 read_data: &ReadData,
7079 ) {
7080 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
7081 controller.push_action(ControlAction::StartInput {
7082 input: InputKind::Secondary,
7083 target_entity: agent
7084 .target
7085 .as_ref()
7086 .and_then(|t| read_data.uids.get(t.target))
7087 .copied(),
7088 select_pos: None,
7089 });
7090 } else if attack_data.in_min_range() && attack_data.angle < 30.0 {
7091 controller.push_basic_input(InputKind::Primary);
7092 controller.inputs.move_dir = Vec2::zero();
7093 } else {
7094 self.path_toward_target(
7095 agent,
7096 controller,
7097 tgt_data.pos.0,
7098 read_data,
7099 Path::AtTarget,
7100 None,
7101 );
7102 }
7103 }
7104
7105 pub fn handle_cursekeeper_fake_attack(
7106 &self,
7107 controller: &mut Controller,
7108 attack_data: &AttackData,
7109 ) {
7110 if attack_data.dist_sqrd < 25_f32.powi(2) {
7111 controller.push_basic_input(InputKind::Primary);
7112 }
7113 }
7114
7115 pub fn handle_karkatha_attack(
7116 &self,
7117 agent: &mut Agent,
7118 controller: &mut Controller,
7119 attack_data: &AttackData,
7120 tgt_data: &TargetData,
7121 read_data: &ReadData,
7122 _rng: &mut impl RngExt,
7123 ) {
7124 enum ActionStateTimers {
7125 RiposteTimer,
7126 SummonTimer,
7127 }
7128
7129 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] += read_data.dt.0;
7130 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] += read_data.dt.0;
7131 if matches!(self.char_state, CharacterState::RiposteMelee(c) if !matches!(c.stage_section, StageSection::Recover))
7132 {
7133 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] = 0.0;
7135 }
7136 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
7137 {
7138 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] = 0.0;
7140 }
7141 let home = agent.patrol_origin.unwrap_or(self.pos.0);
7143 let dest = if tgt_data.pos.0.z < self.pos.0.z {
7144 home
7145 } else {
7146 tgt_data.pos.0
7147 };
7148 if attack_data.in_min_range() {
7149 if agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] > 3.0 {
7150 controller.push_basic_input(InputKind::Ability(2));
7151 } else {
7152 controller.push_basic_input(InputKind::Primary);
7153 };
7154 } else if attack_data.dist_sqrd < 20.0_f32.powi(2) {
7155 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] > 20.0 {
7156 controller.push_basic_input(InputKind::Ability(1));
7157 } else {
7158 controller.push_basic_input(InputKind::Secondary);
7159 }
7160 } else if attack_data.dist_sqrd < 30.0_f32.powi(2) {
7161 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] < 10.0 {
7162 self.path_toward_target(
7163 agent,
7164 controller,
7165 tgt_data.pos.0,
7166 read_data,
7167 Path::AtTarget,
7168 None,
7169 );
7170 } else {
7171 controller.push_basic_input(InputKind::Ability(0));
7172 }
7173 } else {
7174 self.path_toward_target(agent, controller, dest, read_data, Path::AtTarget, None);
7175 }
7176 }
7177
7178 pub fn handle_dagon_attack(
7179 &self,
7180 agent: &mut Agent,
7181 controller: &mut Controller,
7182 attack_data: &AttackData,
7183 tgt_data: &TargetData,
7184 read_data: &ReadData,
7185 ) {
7186 enum ActionStateTimers {
7187 TimerDagon = 0,
7188 }
7189 let line_of_sight_with_target = || {
7190 entities_have_line_of_sight(
7191 self.pos,
7192 self.body,
7193 self.scale,
7194 tgt_data.pos,
7195 tgt_data.body,
7196 tgt_data.scale,
7197 read_data,
7198 )
7199 };
7200 let home = agent.patrol_origin.unwrap_or(self.pos.0);
7202 let exit = Vec3::new(home.x - 6.0, home.y - 6.0, home.z);
7203 let (station_0, station_1) = (exit + 12.0, exit - 12.0);
7204 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.5 {
7205 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] = 0.0;
7206 }
7207 if !line_of_sight_with_target()
7208 && (tgt_data.pos.0 - exit).xy().magnitude_squared() < (10.0_f32).powi(2)
7209 {
7210 let station = if (tgt_data.pos.0 - station_0).xy().magnitude_squared()
7211 < (tgt_data.pos.0 - station_1).xy().magnitude_squared()
7212 {
7213 station_0
7214 } else {
7215 station_1
7216 };
7217 self.path_toward_target(agent, controller, station, read_data, Path::AtTarget, None);
7218 }
7219 else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
7221 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
7222 controller.push_basic_input(InputKind::Primary);
7223 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7224 } else {
7225 controller.push_basic_input(InputKind::Secondary);
7226 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7227 }
7228 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
7230 controller.inputs.move_dir = Vec2::zero();
7231 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
7232 controller.push_basic_input(InputKind::Primary);
7233 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7234 } else {
7235 controller.push_basic_input(InputKind::Ability(1));
7236 }
7237 } else if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) {
7238 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
7240 controller.push_basic_input(InputKind::Primary);
7241 } else {
7242 controller.push_basic_input(InputKind::Ability(2));
7243 }
7244 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7245 } else if line_of_sight_with_target() {
7246 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
7248 controller.push_basic_input(InputKind::Primary);
7249 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7250 } else {
7251 controller.push_basic_input(InputKind::Ability(0));
7252 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
7253 }
7254 }
7255 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
7257 Path::Separate
7258 } else {
7259 Path::AtTarget
7260 };
7261 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
7262 }
7263
7264 pub fn handle_snaretongue_attack(
7265 &self,
7266 agent: &mut Agent,
7267 controller: &mut Controller,
7268 attack_data: &AttackData,
7269 read_data: &ReadData,
7270 ) {
7271 enum Timers {
7272 TimerAttack = 0,
7273 }
7274 let attack_timer = &mut agent.combat_state.timers[Timers::TimerAttack as usize];
7275 if *attack_timer > 2.5 {
7276 *attack_timer = 0.0;
7277 }
7278 if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
7280 if *attack_timer > 0.5 {
7281 controller.push_basic_input(InputKind::Primary);
7282 *attack_timer += read_data.dt.0;
7283 } else {
7284 controller.push_basic_input(InputKind::Secondary);
7285 *attack_timer += read_data.dt.0;
7286 }
7287 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
7289 controller.inputs.move_dir = Vec2::zero();
7290 if *attack_timer > 2.0 {
7291 controller.push_basic_input(InputKind::Ability(0));
7292 *attack_timer += read_data.dt.0;
7293 } else {
7294 controller.push_basic_input(InputKind::Ability(1));
7295 }
7296 } else {
7297 if *attack_timer > 1.0 {
7299 controller.push_basic_input(InputKind::Ability(0));
7300 *attack_timer += read_data.dt.0;
7301 } else {
7302 controller.push_basic_input(InputKind::Ability(2));
7303 *attack_timer += read_data.dt.0;
7304 }
7305 }
7306 }
7307
7308 pub fn handle_deadwood(
7309 &self,
7310 agent: &mut Agent,
7311 controller: &mut Controller,
7312 attack_data: &AttackData,
7313 tgt_data: &TargetData,
7314 read_data: &ReadData,
7315 ) {
7316 const BEAM_RANGE: f32 = 20.0;
7317 const BEAM_TIME: Duration = Duration::from_secs(3);
7318 if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
7320 {
7321 controller.push_basic_input(InputKind::Secondary);
7323 controller.inputs.move_dir = self.ori.look_vec().xy();
7324 } else if attack_data.in_min_range() && attack_data.angle_xy < 10.0 {
7325 controller.push_basic_input(InputKind::Secondary);
7327 } else if matches!(self.char_state, CharacterState::BasicBeam(s) if s.stage_section != StageSection::Recover && s.timer < BEAM_TIME)
7328 {
7329 controller.push_basic_input(InputKind::Primary);
7331 } else if attack_data.dist_sqrd < BEAM_RANGE.powi(2) {
7332 if attack_data.angle_xy < 5.0 {
7334 controller.push_basic_input(InputKind::Primary);
7335 } else {
7336 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7338 .xy()
7339 .try_normalized()
7340 .unwrap_or_else(Vec2::zero)
7341 * 0.01;
7342 }
7343 } else {
7344 self.path_toward_target(
7346 agent,
7347 controller,
7348 tgt_data.pos.0,
7349 read_data,
7350 Path::AtTarget,
7351 None,
7352 );
7353 }
7354 }
7355
7356 pub fn handle_mandragora(
7357 &self,
7358 agent: &mut Agent,
7359 controller: &mut Controller,
7360 attack_data: &AttackData,
7361 tgt_data: &TargetData,
7362 read_data: &ReadData,
7363 ) {
7364 const SCREAM_RANGE: f32 = 10.0; enum ActionStateFCounters {
7367 FCounterHealthThreshold = 0,
7368 }
7369
7370 enum ActionStateConditions {
7371 ConditionHasScreamed = 0,
7372 }
7373
7374 if !agent.combat_state.initialized {
7375 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
7376 self.health.map_or(0.0, |h| h.maximum());
7377 agent.combat_state.initialized = true;
7378 }
7379
7380 if !agent.combat_state.conditions[ActionStateConditions::ConditionHasScreamed as usize] {
7381 if self.health.is_some_and(|h| {
7384 h.current()
7385 < agent.combat_state.counters
7386 [ActionStateFCounters::FCounterHealthThreshold as usize]
7387 }) || attack_data.dist_sqrd < SCREAM_RANGE.powi(2)
7388 {
7389 agent.combat_state.conditions
7390 [ActionStateConditions::ConditionHasScreamed as usize] = true;
7391 controller.push_basic_input(InputKind::Secondary);
7392 }
7393 } else {
7394 if attack_data.in_min_range() {
7396 controller.push_basic_input(InputKind::Primary);
7397 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
7398 && entities_have_line_of_sight(
7399 self.pos,
7400 self.body,
7401 self.scale,
7402 tgt_data.pos,
7403 tgt_data.body,
7404 tgt_data.scale,
7405 read_data,
7406 )
7407 {
7408 self.path_toward_target(
7410 agent,
7411 controller,
7412 tgt_data.pos.0,
7413 read_data,
7414 Path::AtTarget,
7415 None,
7416 );
7417 } else {
7418 agent.combat_state.conditions
7420 [ActionStateConditions::ConditionHasScreamed as usize] = false;
7421 agent.combat_state.counters
7422 [ActionStateFCounters::FCounterHealthThreshold as usize] =
7423 self.health.map_or(0.0, |h| h.maximum());
7424 }
7425 }
7426 }
7427
7428 pub fn handle_wood_golem(
7429 &self,
7430 agent: &mut Agent,
7431 controller: &mut Controller,
7432 attack_data: &AttackData,
7433 tgt_data: &TargetData,
7434 read_data: &ReadData,
7435 rng: &mut impl RngExt,
7436 ) {
7437 const PATH_RANGE_FACTOR: f32 = 0.3; const STRIKE_RANGE_FACTOR: f32 = 0.6; const STRIKE_AIM_FACTOR: f32 = 0.7;
7452 const SPIN_RANGE_FACTOR: f32 = 0.6;
7453 const SPIN_COOLDOWN: f32 = 1.5;
7454 const SPIN_RELAX_FACTOR: f32 = 0.2;
7455 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.7;
7456 const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
7457 const SHOCKWAVE_COOLDOWN: f32 = 5.0;
7458 const MIXUP_COOLDOWN: f32 = 2.5;
7459 const MIXUP_RELAX_FACTOR: f32 = 0.3;
7460
7461 const SPIN: usize = 0;
7463 const SHOCKWAVE: usize = 1;
7464 const MIXUP: usize = 2;
7465
7466 let shockwave_min_range = self.body.map_or(0.0, |b| b.height() * 1.1);
7469
7470 let (strike_range, strike_angle) = {
7472 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
7473 self.extract_ability(AbilityInput::Primary)
7474 {
7475 (range, angle)
7476 } else {
7477 (0.0, 0.0)
7478 }
7479 };
7480 let spin_range = {
7481 if let Some(AbilityData::BasicMelee { range, .. }) =
7482 self.extract_ability(AbilityInput::Secondary)
7483 {
7484 range
7485 } else {
7486 0.0
7487 }
7488 };
7489 let (shockwave_max_range, shockwave_angle) = {
7490 if let Some(AbilityData::Shockwave { range, angle, .. }) =
7491 self.extract_ability(AbilityInput::Auxiliary(0))
7492 {
7493 (range, angle)
7494 } else {
7495 (0.0, 0.0)
7496 }
7497 };
7498
7499 let is_in_spin_range = attack_data.dist_sqrd
7501 < (attack_data.body_dist + spin_range * SPIN_RANGE_FACTOR).powi(2);
7502 let is_in_strike_range = attack_data.dist_sqrd
7503 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7504 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7505
7506 let current_input = self.char_state.ability_info().map(|ai| ai.input);
7511 if matches!(current_input, Some(InputKind::Secondary)) {
7512 agent.combat_state.timers[SPIN] = 0.0;
7514 agent.combat_state.timers[MIXUP] = 0.0;
7515 } else if is_in_spin_range && !(is_in_strike_range && is_in_strike_angle) {
7516 agent.combat_state.timers[SPIN] += read_data.dt.0;
7518 } else {
7519 agent.combat_state.timers[SPIN] =
7521 (agent.combat_state.timers[SPIN] - read_data.dt.0 * SPIN_RELAX_FACTOR).max(0.0);
7522 }
7523 if matches!(self.char_state, CharacterState::Shockwave(_)) {
7525 agent.combat_state.timers[SHOCKWAVE] = 0.0;
7527 agent.combat_state.timers[MIXUP] = 0.0;
7528 } else {
7529 agent.combat_state.timers[SHOCKWAVE] += read_data.dt.0;
7531 }
7532 if is_in_strike_range && is_in_strike_angle {
7534 agent.combat_state.timers[MIXUP] += read_data.dt.0;
7536 } else {
7537 agent.combat_state.timers[MIXUP] =
7539 (agent.combat_state.timers[MIXUP] - read_data.dt.0 * MIXUP_RELAX_FACTOR).max(0.0);
7540 }
7541
7542 if is_in_strike_range && is_in_strike_angle {
7545 if agent.combat_state.timers[MIXUP] > MIXUP_COOLDOWN {
7547 let randomise: u8 = rng.random_range(1..=3);
7548 match randomise {
7549 1 => controller.push_basic_input(InputKind::Ability(0)), 2 => controller.push_basic_input(InputKind::Primary), _ => controller.push_basic_input(InputKind::Secondary), }
7553 }
7554 else {
7556 controller.push_basic_input(InputKind::Primary);
7557 }
7558 }
7559 else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
7561 if agent.combat_state.timers[SPIN] > SPIN_COOLDOWN {
7563 controller.push_basic_input(InputKind::Secondary);
7564 }
7565 }
7567 else if attack_data.dist_sqrd > shockwave_min_range.powi(2)
7569 && attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7570 && attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
7571 {
7572 if agent.combat_state.timers[SHOCKWAVE] > SHOCKWAVE_COOLDOWN {
7574 controller.push_basic_input(InputKind::Ability(0));
7575 }
7576 }
7578
7579 if attack_data.dist_sqrd
7582 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7583 {
7584 self.path_toward_target(
7585 agent,
7586 controller,
7587 tgt_data.pos.0,
7588 read_data,
7589 Path::AtTarget,
7590 None,
7591 );
7592 }
7593 else if attack_data.angle > 0.0 {
7595 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7597 .xy()
7598 .try_normalized()
7599 .unwrap_or_else(Vec2::zero)
7600 * 0.001; }
7602 }
7603
7604 pub fn handle_gnarling_chieftain(
7605 &self,
7606 agent: &mut Agent,
7607 controller: &mut Controller,
7608 attack_data: &AttackData,
7609 tgt_data: &TargetData,
7610 read_data: &ReadData,
7611 rng: &mut impl RngExt,
7612 ) {
7613 const PATH_RANGE_FACTOR: f32 = 0.4;
7628 const STRIKE_RANGE_FACTOR: f32 = 0.7;
7629 const STRIKE_AIM_FACTOR: f32 = 0.8;
7630 const BARRAGE_RANGE_FACTOR: f32 = 0.8;
7631 const BARRAGE_AIM_FACTOR: f32 = 0.65;
7632 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
7633 const TOTEM_COOLDOWN: f32 = 25.0;
7634 const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
7635 const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
7636 const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
7637
7638 const HAS_SUMMONED_FIRST_TOTEM: usize = 0;
7640 const SUMMON_TOTEM: usize = 0;
7642 const HEAVY_ATTACK: usize = 1;
7643 const HEAVY_ATTACK_COOLDOWN: usize = 0;
7645
7646 let line_of_sight_with_target = || {
7648 entities_have_line_of_sight(
7649 self.pos,
7650 self.body,
7651 self.scale,
7652 tgt_data.pos,
7653 tgt_data.body,
7654 tgt_data.scale,
7655 read_data,
7656 )
7657 };
7658
7659 let (strike_range, strike_angle) = {
7662 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
7663 self.extract_ability(AbilityInput::Primary)
7664 {
7665 (range, angle)
7666 } else {
7667 (0.0, 0.0)
7668 }
7669 };
7670 let (barrage_speed, barrage_spread, barrage_count) = {
7671 if let Some(AbilityData::BasicRanged {
7672 projectile_speed,
7673 projectile_spread,
7674 num_projectiles,
7675 ..
7676 }) = self.extract_ability(AbilityInput::Secondary)
7677 {
7678 (
7679 projectile_speed,
7680 projectile_spread,
7681 num_projectiles.compute(self.heads.map_or(1, |heads| heads.amount() as u32)),
7682 )
7683 } else {
7684 (0.0, 0.0, 0)
7685 }
7686 };
7687 let shockwave_range = {
7688 if let Some(AbilityData::Shockwave { range, .. }) =
7689 self.extract_ability(AbilityInput::Auxiliary(0))
7690 {
7691 range
7692 } else {
7693 0.0
7694 }
7695 };
7696
7697 let barrage_max_range =
7699 projectile_flat_range(barrage_speed, self.body.map_or(2.0, |b| b.height()));
7700 let barrange_angle = projectile_multi_angle(barrage_spread, barrage_count);
7701
7702 let is_in_strike_range = attack_data.dist_sqrd
7704 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7705 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7706
7707 if !agent.combat_state.initialized {
7709 agent.combat_state.initialized = true;
7710 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7711 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7712 }
7713
7714 match self.char_state {
7719 CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
7720 agent.combat_state.timers[SUMMON_TOTEM] = 0.0;
7722 agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] = true;
7723 },
7724 CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
7725 agent.combat_state.counters[HEAVY_ATTACK] = 0.0;
7727 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7728 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7729 },
7730 _ => {},
7731 }
7732 agent.combat_state.timers[SUMMON_TOTEM] += read_data.dt.0;
7734 if is_in_strike_range {
7736 if is_in_strike_angle {
7738 agent.combat_state.counters[HEAVY_ATTACK] += read_data.dt.0;
7739 } else {
7740 agent.combat_state.counters[HEAVY_ATTACK] +=
7742 read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
7743 }
7744 } else {
7745 agent.combat_state.counters[HEAVY_ATTACK] +=
7747 read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
7748 }
7749
7750 if !agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] {
7753 controller.push_basic_input(InputKind::Ability(2));
7754 }
7755 else if agent.combat_state.timers[SUMMON_TOTEM] > TOTEM_COOLDOWN {
7757 controller.push_basic_input(InputKind::Ability(rng.random_range(1..=3)));
7758 }
7759 else if agent.combat_state.counters[HEAVY_ATTACK]
7763 > agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN]
7764 && attack_data.dist_sqrd < (barrage_max_range * BARRAGE_RANGE_FACTOR).powi(2)
7765 {
7766 if line_of_sight_with_target() {
7768 if attack_data.angle > barrange_angle * BARRAGE_AIM_FACTOR {
7770 controller.push_basic_input(InputKind::Ability(0));
7771 }
7772 else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7774 {
7775 if rng.random_bool(0.5) {
7776 controller.push_basic_input(InputKind::Secondary);
7777 } else {
7778 controller.push_basic_input(InputKind::Ability(0));
7779 }
7780 }
7781 else {
7783 controller.push_basic_input(InputKind::Secondary);
7784 }
7785 }
7787 else {
7789 if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
7791 controller.push_basic_input(InputKind::Ability(0));
7792 }
7793 }
7795 }
7796 else if is_in_strike_range && is_in_strike_angle {
7798 controller.push_basic_input(InputKind::Primary);
7799 }
7800 if attack_data.dist_sqrd
7805 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7806 {
7807 self.path_toward_target(
7808 agent,
7809 controller,
7810 tgt_data.pos.0,
7811 read_data,
7812 Path::AtTarget,
7813 None,
7814 );
7815 }
7816 else if attack_data.angle > 0.0 {
7818 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7820 .xy()
7821 .try_normalized()
7822 .unwrap_or_else(Vec2::zero)
7823 * 0.001; }
7825 }
7826
7827 pub fn handle_sword_simple_attack(
7828 &self,
7829 agent: &mut Agent,
7830 controller: &mut Controller,
7831 attack_data: &AttackData,
7832 tgt_data: &TargetData,
7833 read_data: &ReadData,
7834 ) {
7835 const DASH_TIMER: usize = 0;
7836 agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
7837 if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
7838 {
7839 controller.push_basic_input(InputKind::Secondary);
7840 } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
7841 if agent.combat_state.timers[DASH_TIMER] > 2.0 {
7842 agent.combat_state.timers[DASH_TIMER] = 0.0;
7843 }
7844 controller.push_basic_input(InputKind::Primary);
7845 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
7846 && self
7847 .path_toward_target(
7848 agent,
7849 controller,
7850 tgt_data.pos.0,
7851 read_data,
7852 Path::Separate,
7853 None,
7854 )
7855 .is_some()
7856 && entities_have_line_of_sight(
7857 self.pos,
7858 self.body,
7859 self.scale,
7860 tgt_data.pos,
7861 tgt_data.body,
7862 tgt_data.scale,
7863 read_data,
7864 )
7865 && agent.combat_state.timers[DASH_TIMER] > 4.0
7866 && attack_data.angle < 45.0
7867 {
7868 controller.push_basic_input(InputKind::Secondary);
7869 agent.combat_state.timers[DASH_TIMER] = 0.0;
7870 } else {
7871 self.path_toward_target(
7872 agent,
7873 controller,
7874 tgt_data.pos.0,
7875 read_data,
7876 Path::AtTarget,
7877 None,
7878 );
7879 }
7880 }
7881
7882 pub fn handle_adlet_hunter(
7883 &self,
7884 agent: &mut Agent,
7885 controller: &mut Controller,
7886 attack_data: &AttackData,
7887 tgt_data: &TargetData,
7888 read_data: &ReadData,
7889 rng: &mut impl RngExt,
7890 ) {
7891 const ROTATE_TIMER: usize = 0;
7892 const ROTATE_DIR_CONDITION: usize = 0;
7893 agent.combat_state.timers[ROTATE_TIMER] -= read_data.dt.0;
7894 if agent.combat_state.timers[ROTATE_TIMER] < 0.0 {
7895 agent.combat_state.conditions[ROTATE_DIR_CONDITION] = rng.random_bool(0.5);
7896 agent.combat_state.timers[ROTATE_TIMER] = rng.random::<f32>() * 5.0;
7897 }
7898 let primary = self.extract_ability(AbilityInput::Primary);
7899 let secondary = self.extract_ability(AbilityInput::Secondary);
7900 let could_use_input = |input| match input {
7901 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7902 p.could_use(
7903 attack_data,
7904 self,
7905 tgt_data,
7906 read_data,
7907 AbilityPreferences::default(),
7908 )
7909 }),
7910 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7911 s.could_use(
7912 attack_data,
7913 self,
7914 tgt_data,
7915 read_data,
7916 AbilityPreferences::default(),
7917 )
7918 }),
7919 _ => false,
7920 };
7921 let move_forwards = if could_use_input(InputKind::Primary) {
7922 controller.push_basic_input(InputKind::Primary);
7923 false
7924 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 8_f32.powi(2) {
7925 controller.push_basic_input(InputKind::Secondary);
7926 true
7927 } else {
7928 true
7929 };
7930
7931 if move_forwards && attack_data.dist_sqrd > 3_f32.powi(2) {
7932 self.path_toward_target(
7933 agent,
7934 controller,
7935 tgt_data.pos.0,
7936 read_data,
7937 Path::Separate,
7938 None,
7939 );
7940 } else {
7941 self.path_toward_target(
7942 agent,
7943 controller,
7944 tgt_data.pos.0,
7945 read_data,
7946 Path::Separate,
7947 None,
7948 );
7949 let dir = if agent.combat_state.conditions[ROTATE_DIR_CONDITION] {
7950 1.0
7951 } else {
7952 -1.0
7953 };
7954 controller.inputs.move_dir.rotate_z(PI / 2.0 * dir);
7955 }
7956 }
7957
7958 pub fn handle_adlet_icepicker(
7959 &self,
7960 agent: &mut Agent,
7961 controller: &mut Controller,
7962 attack_data: &AttackData,
7963 tgt_data: &TargetData,
7964 read_data: &ReadData,
7965 ) {
7966 let primary = self.extract_ability(AbilityInput::Primary);
7967 let secondary = self.extract_ability(AbilityInput::Secondary);
7968 let could_use_input = |input| match input {
7969 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7970 p.could_use(
7971 attack_data,
7972 self,
7973 tgt_data,
7974 read_data,
7975 AbilityPreferences::default(),
7976 )
7977 }),
7978 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7979 s.could_use(
7980 attack_data,
7981 self,
7982 tgt_data,
7983 read_data,
7984 AbilityPreferences::default(),
7985 )
7986 }),
7987 _ => false,
7988 };
7989 let move_forwards = if could_use_input(InputKind::Primary) {
7990 controller.push_basic_input(InputKind::Primary);
7991 false
7992 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 5_f32.powi(2) {
7993 controller.push_basic_input(InputKind::Secondary);
7994 false
7995 } else {
7996 true
7997 };
7998
7999 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
8000 self.path_toward_target(
8001 agent,
8002 controller,
8003 tgt_data.pos.0,
8004 read_data,
8005 Path::Separate,
8006 None,
8007 );
8008 }
8009 }
8010
8011 pub fn handle_adlet_tracker(
8012 &self,
8013 agent: &mut Agent,
8014 controller: &mut Controller,
8015 attack_data: &AttackData,
8016 tgt_data: &TargetData,
8017 read_data: &ReadData,
8018 ) {
8019 const TRAP_TIMER: usize = 0;
8020 agent.combat_state.timers[TRAP_TIMER] += read_data.dt.0;
8021 if agent.combat_state.timers[TRAP_TIMER] > 20.0 {
8022 agent.combat_state.timers[TRAP_TIMER] = 0.0;
8023 }
8024 let primary = self.extract_ability(AbilityInput::Primary);
8025 let could_use_input = |input| match input {
8026 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8027 p.could_use(
8028 attack_data,
8029 self,
8030 tgt_data,
8031 read_data,
8032 AbilityPreferences::default(),
8033 )
8034 }),
8035 _ => false,
8036 };
8037 let move_forwards = if agent.combat_state.timers[TRAP_TIMER] < 3.0 {
8038 controller.push_basic_input(InputKind::Secondary);
8039 false
8040 } else if could_use_input(InputKind::Primary) {
8041 controller.push_basic_input(InputKind::Primary);
8042 false
8043 } else {
8044 true
8045 };
8046
8047 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
8048 self.path_toward_target(
8049 agent,
8050 controller,
8051 tgt_data.pos.0,
8052 read_data,
8053 Path::Separate,
8054 None,
8055 );
8056 }
8057 }
8058
8059 pub fn handle_adlet_elder(
8060 &self,
8061 agent: &mut Agent,
8062 controller: &mut Controller,
8063 attack_data: &AttackData,
8064 tgt_data: &TargetData,
8065 read_data: &ReadData,
8066 rng: &mut impl RngExt,
8067 ) {
8068 const TRAP_TIMER: usize = 0;
8069 agent.combat_state.timers[TRAP_TIMER] -= read_data.dt.0;
8070 if matches!(self.char_state, CharacterState::BasicRanged(_)) {
8071 agent.combat_state.timers[TRAP_TIMER] = 15.0;
8072 }
8073 let primary = self.extract_ability(AbilityInput::Primary);
8074 let secondary = self.extract_ability(AbilityInput::Secondary);
8075 let abilities = [
8076 self.extract_ability(AbilityInput::Auxiliary(0)),
8077 self.extract_ability(AbilityInput::Auxiliary(1)),
8078 ];
8079 let could_use_input = |input| match input {
8080 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8081 p.could_use(
8082 attack_data,
8083 self,
8084 tgt_data,
8085 read_data,
8086 AbilityPreferences::default(),
8087 )
8088 }),
8089 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8090 s.could_use(
8091 attack_data,
8092 self,
8093 tgt_data,
8094 read_data,
8095 AbilityPreferences::default(),
8096 )
8097 }),
8098 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8099 a.could_use(
8100 attack_data,
8101 self,
8102 tgt_data,
8103 read_data,
8104 AbilityPreferences::default(),
8105 )
8106 }),
8107 _ => false,
8108 };
8109 let move_forwards = if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
8110 {
8111 controller.push_basic_input(InputKind::Secondary);
8112 false
8113 } else if agent.combat_state.timers[TRAP_TIMER] < 0.0 && !tgt_data.considered_ranged() {
8114 controller.push_basic_input(InputKind::Ability(0));
8115 false
8116 } else if could_use_input(InputKind::Primary) {
8117 controller.push_basic_input(InputKind::Primary);
8118 false
8119 } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.5) {
8120 controller.push_basic_input(InputKind::Secondary);
8121 false
8122 } else if could_use_input(InputKind::Ability(1)) {
8123 controller.push_basic_input(InputKind::Ability(1));
8124 false
8125 } else {
8126 true
8127 };
8128
8129 if matches!(self.char_state, CharacterState::LeapMelee(_)) {
8130 let tgt_vec = tgt_data.pos.0.xy() - self.pos.0.xy();
8131 if tgt_vec.magnitude_squared() > 2_f32.powi(2)
8132 && let Some(look_dir) = Dir::from_unnormalized(Vec3::from(tgt_vec))
8133 {
8134 controller.inputs.look_dir = look_dir;
8135 }
8136 }
8137
8138 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
8139 self.path_toward_target(
8140 agent,
8141 controller,
8142 tgt_data.pos.0,
8143 read_data,
8144 Path::Separate,
8145 None,
8146 );
8147 }
8148 }
8149
8150 pub fn handle_icedrake(
8151 &self,
8152 agent: &mut Agent,
8153 controller: &mut Controller,
8154 attack_data: &AttackData,
8155 tgt_data: &TargetData,
8156 read_data: &ReadData,
8157 rng: &mut impl RngExt,
8158 ) {
8159 let primary = self.extract_ability(AbilityInput::Primary);
8160 let secondary = self.extract_ability(AbilityInput::Secondary);
8161 let abilities = [
8162 self.extract_ability(AbilityInput::Auxiliary(0)),
8163 self.extract_ability(AbilityInput::Auxiliary(1)),
8164 ];
8165 let could_use_input = |input| match input {
8166 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8167 p.could_use(
8168 attack_data,
8169 self,
8170 tgt_data,
8171 read_data,
8172 AbilityPreferences::default(),
8173 )
8174 }),
8175 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8176 s.could_use(
8177 attack_data,
8178 self,
8179 tgt_data,
8180 read_data,
8181 AbilityPreferences::default(),
8182 )
8183 }),
8184 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8185 a.could_use(
8186 attack_data,
8187 self,
8188 tgt_data,
8189 read_data,
8190 AbilityPreferences::default(),
8191 )
8192 }),
8193 _ => false,
8194 };
8195
8196 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
8197 Some(input @ InputKind::Primary) => {
8198 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
8199 && could_use_input(input)
8200 {
8201 controller.push_basic_input(input);
8202 true
8203 } else {
8204 false
8205 }
8206 },
8207 Some(input @ InputKind::Ability(1))
8208 if self
8209 .char_state
8210 .timer()
8211 .is_some_and(|t| t.as_secs_f32() < 3.0)
8212 && could_use_input(input) =>
8213 {
8214 controller.push_basic_input(input);
8215 true
8216 },
8217 _ => false,
8218 };
8219
8220 let move_forwards = if !continued_attack {
8221 if could_use_input(InputKind::Primary) && rng.random_bool(0.4) {
8222 controller.push_basic_input(InputKind::Primary);
8223 false
8224 } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.8) {
8225 controller.push_basic_input(InputKind::Secondary);
8226 false
8227 } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(0.9) {
8228 controller.push_basic_input(InputKind::Ability(1));
8229 true
8230 } else if could_use_input(InputKind::Ability(0)) {
8231 controller.push_basic_input(InputKind::Ability(0));
8232 true
8233 } else {
8234 true
8235 }
8236 } else {
8237 false
8238 };
8239
8240 if move_forwards {
8241 self.path_toward_target(
8242 agent,
8243 controller,
8244 tgt_data.pos.0,
8245 read_data,
8246 Path::Separate,
8247 None,
8248 );
8249 }
8250 }
8251
8252 pub fn handle_hydra(
8253 &self,
8254 agent: &mut Agent,
8255 controller: &mut Controller,
8256 attack_data: &AttackData,
8257 tgt_data: &TargetData,
8258 read_data: &ReadData,
8259 rng: &mut impl RngExt,
8260 ) {
8261 enum ActionStateTimers {
8262 RegrowHeadNoDamage,
8263 RegrowHeadNoAttack,
8264 }
8265
8266 let could_use_input = |input| {
8267 Option::from(input)
8268 .and_then(|ability| {
8269 Some(self.extract_ability(ability)?.could_use(
8270 attack_data,
8271 self,
8272 tgt_data,
8273 read_data,
8274 AbilityPreferences::default(),
8275 ))
8276 })
8277 .unwrap_or(false)
8278 };
8279
8280 const FOCUS_ATTACK_RANGE: f32 = 5.0;
8281
8282 if attack_data.dist_sqrd < FOCUS_ATTACK_RANGE.powi(2) {
8283 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] = 0.0;
8284 } else {
8285 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] +=
8286 read_data.dt.0;
8287 }
8288
8289 if let Some(health) = self.health.filter(|health| health.last_change.amount < 0.0) {
8290 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] =
8291 (read_data.time.0 - health.last_change.time.0) as f32;
8292 } else {
8293 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] +=
8294 read_data.dt.0;
8295 }
8296
8297 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
8298 match self.char_state {
8299 CharacterState::ChargedMelee(c) => {
8300 if c.charge_frac() < 1.0 && could_use_input(input) {
8301 controller.push_basic_input(input);
8302 }
8303 },
8304 CharacterState::ChargedRanged(c)
8305 if c.charge_frac() < 1.0 && could_use_input(input) =>
8306 {
8307 controller.push_basic_input(input);
8308 },
8309 _ => {},
8310 }
8311 }
8312
8313 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
8314 Some(input @ InputKind::Primary)
8315 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
8316 && could_use_input(input) =>
8317 {
8318 controller.push_basic_input(input);
8319 true
8320 },
8321 _ => false,
8322 };
8323
8324 let has_heads = self.heads.is_none_or(|heads| heads.amount() > 0);
8325
8326 let move_forwards = if !continued_attack {
8327 if could_use_input(InputKind::Ability(1))
8328 && rng.random_bool(0.9)
8329 && (agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] > 5.0
8330 || agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize]
8331 > 6.0)
8332 && self.heads.is_some_and(|heads| heads.amount_missing() > 0)
8333 {
8334 controller.push_basic_input(InputKind::Ability(2));
8335 false
8336 } else if has_heads && could_use_input(InputKind::Primary) && rng.random_bool(0.8) {
8337 controller.push_basic_input(InputKind::Primary);
8338 true
8339 } else if has_heads && could_use_input(InputKind::Secondary) && rng.random_bool(0.4) {
8340 controller.push_basic_input(InputKind::Secondary);
8341 false
8342 } else if has_heads && could_use_input(InputKind::Ability(1)) && rng.random_bool(0.6) {
8343 controller.push_basic_input(InputKind::Ability(1));
8344 true
8345 } else if !has_heads && could_use_input(InputKind::Ability(3)) && rng.random_bool(0.7) {
8346 controller.push_basic_input(InputKind::Ability(3));
8347 true
8348 } else if could_use_input(InputKind::Ability(0)) {
8349 controller.push_basic_input(InputKind::Ability(0));
8350 true
8351 } else {
8352 true
8353 }
8354 } else {
8355 true
8356 };
8357
8358 if move_forwards {
8359 if has_heads {
8360 self.path_toward_target(
8361 agent,
8362 controller,
8363 tgt_data.pos.0,
8364 read_data,
8365 Path::Separate,
8366 (attack_data.dist_sqrd
8368 < (2.5 + self.body.map_or(0.0, |b| b.front_radius())).powi(2))
8369 .then_some(0.3),
8370 );
8371 } else {
8372 self.flee(agent, controller, read_data, tgt_data.pos);
8373 }
8374 }
8375 }
8376
8377 pub fn handle_random_abilities(
8378 &self,
8379 agent: &mut Agent,
8380 controller: &mut Controller,
8381 attack_data: &AttackData,
8382 tgt_data: &TargetData,
8383 read_data: &ReadData,
8384 rng: &mut impl RngExt,
8385 primary_weight: u8,
8386 secondary_weight: u8,
8387 ability_weights: [u8; BASE_ABILITY_LIMIT],
8388 ) {
8389 let primary = self.extract_ability(AbilityInput::Primary);
8390 let secondary = self.extract_ability(AbilityInput::Secondary);
8391 let abilities = [
8392 self.extract_ability(AbilityInput::Auxiliary(0)),
8393 self.extract_ability(AbilityInput::Auxiliary(1)),
8394 self.extract_ability(AbilityInput::Auxiliary(2)),
8395 self.extract_ability(AbilityInput::Auxiliary(3)),
8396 self.extract_ability(AbilityInput::Auxiliary(4)),
8397 ];
8398 let could_use_input = |input| match input {
8399 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8400 p.could_use(
8401 attack_data,
8402 self,
8403 tgt_data,
8404 read_data,
8405 AbilityPreferences::default(),
8406 )
8407 }),
8408 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8409 s.could_use(
8410 attack_data,
8411 self,
8412 tgt_data,
8413 read_data,
8414 AbilityPreferences::default(),
8415 )
8416 }),
8417 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8418 a.could_use(
8419 attack_data,
8420 self,
8421 tgt_data,
8422 read_data,
8423 AbilityPreferences::default(),
8424 )
8425 }),
8426 _ => false,
8427 };
8428
8429 let primary_chance = primary_weight as f64
8430 / ((primary_weight + secondary_weight + ability_weights.iter().sum::<u8>()) as f64)
8431 .max(0.01);
8432 let secondary_chance = secondary_weight as f64
8433 / ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
8434 let ability_chances = {
8435 let mut chances = [0.0; BASE_ABILITY_LIMIT];
8436 chances.iter_mut().enumerate().for_each(|(i, chance)| {
8437 *chance = ability_weights[i] as f64
8438 / (ability_weights
8439 .iter()
8440 .enumerate()
8441 .filter_map(|(j, weight)| if j >= i { Some(weight) } else { None })
8442 .sum::<u8>() as f64)
8443 .max(0.01)
8444 });
8445 chances
8446 };
8447
8448 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
8449 match self.char_state {
8450 CharacterState::ChargedMelee(c) => {
8451 if c.charge_frac() < 1.0 && could_use_input(input) {
8452 controller.push_basic_input(input);
8453 }
8454 },
8455 CharacterState::ChargedRanged(c)
8456 if c.charge_frac() < 1.0 && could_use_input(input) =>
8457 {
8458 controller.push_basic_input(input);
8459 },
8460 _ => {},
8461 }
8462 }
8463
8464 let move_forwards = if could_use_input(InputKind::Primary)
8465 && rng.random_bool(primary_chance)
8466 {
8467 controller.push_basic_input(InputKind::Primary);
8468 false
8469 } else if could_use_input(InputKind::Secondary) && rng.random_bool(secondary_chance) {
8470 controller.push_basic_input(InputKind::Secondary);
8471 false
8472 } else if could_use_input(InputKind::Ability(0)) && rng.random_bool(ability_chances[0]) {
8473 controller.push_basic_input(InputKind::Ability(0));
8474 false
8475 } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(ability_chances[1]) {
8476 controller.push_basic_input(InputKind::Ability(1));
8477 false
8478 } else if could_use_input(InputKind::Ability(2)) && rng.random_bool(ability_chances[2]) {
8479 controller.push_basic_input(InputKind::Ability(2));
8480 false
8481 } else if could_use_input(InputKind::Ability(3)) && rng.random_bool(ability_chances[3]) {
8482 controller.push_basic_input(InputKind::Ability(3));
8483 false
8484 } else if could_use_input(InputKind::Ability(4)) && rng.random_bool(ability_chances[4]) {
8485 controller.push_basic_input(InputKind::Ability(4));
8486 false
8487 } else {
8488 true
8489 };
8490
8491 if move_forwards {
8492 self.path_toward_target(
8493 agent,
8494 controller,
8495 tgt_data.pos.0,
8496 read_data,
8497 Path::Separate,
8498 None,
8499 );
8500 }
8501 }
8502
8503 pub fn handle_simple_double_attack(
8504 &self,
8505 agent: &mut Agent,
8506 controller: &mut Controller,
8507 attack_data: &AttackData,
8508 tgt_data: &TargetData,
8509 read_data: &ReadData,
8510 ) {
8511 const MAX_ATTACK_RANGE: f32 = 20.0;
8512
8513 if attack_data.angle < 60.0 && attack_data.dist_sqrd < MAX_ATTACK_RANGE.powi(2) {
8514 controller.inputs.move_dir = Vec2::zero();
8515 if attack_data.in_min_range() {
8516 controller.push_basic_input(InputKind::Primary);
8517 } else {
8518 controller.push_basic_input(InputKind::Secondary);
8519 }
8520 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8521 self.path_toward_target(
8522 agent,
8523 controller,
8524 tgt_data.pos.0,
8525 read_data,
8526 Path::Separate,
8527 None,
8528 );
8529 } else {
8530 self.path_toward_target(
8531 agent,
8532 controller,
8533 tgt_data.pos.0,
8534 read_data,
8535 Path::AtTarget,
8536 None,
8537 );
8538 }
8539 }
8540
8541 pub fn handle_clay_steed_attack(
8542 &self,
8543 agent: &mut Agent,
8544 controller: &mut Controller,
8545 attack_data: &AttackData,
8546 tgt_data: &TargetData,
8547 read_data: &ReadData,
8548 ) {
8549 enum ActionStateTimers {
8550 AttackTimer,
8551 }
8552 const HOOF_ATTACK_RANGE: f32 = 1.0;
8553 const HOOF_ATTACK_ANGLE: f32 = 50.0;
8554
8555 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8556 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 10.0 {
8557 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8559 }
8560
8561 if attack_data.angle < HOOF_ATTACK_ANGLE
8562 && attack_data.dist_sqrd
8563 < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.max_radius())).powi(2)
8564 {
8565 controller.inputs.move_dir = Vec2::zero();
8566 controller.push_basic_input(InputKind::Primary);
8567 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
8568 controller.push_basic_input(InputKind::Secondary);
8569 } else {
8570 self.path_toward_target(
8571 agent,
8572 controller,
8573 tgt_data.pos.0,
8574 read_data,
8575 Path::AtTarget,
8576 None,
8577 );
8578 }
8579 }
8580
8581 pub fn handle_ancient_effigy_attack(
8582 &self,
8583 agent: &mut Agent,
8584 controller: &mut Controller,
8585 attack_data: &AttackData,
8586 tgt_data: &TargetData,
8587 read_data: &ReadData,
8588 ) {
8589 enum ActionStateTimers {
8590 BlastTimer,
8591 }
8592
8593 let home = agent.patrol_origin.unwrap_or(self.pos.0);
8594 let line_of_sight_with_target = || {
8595 entities_have_line_of_sight(
8596 self.pos,
8597 self.body,
8598 self.scale,
8599 tgt_data.pos,
8600 tgt_data.body,
8601 tgt_data.scale,
8602 read_data,
8603 )
8604 };
8605 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] += read_data.dt.0;
8606
8607 if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] > 6.0 {
8608 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] = 0.0;
8609 }
8610 if line_of_sight_with_target() {
8611 if attack_data.in_min_range() {
8612 controller.push_basic_input(InputKind::Secondary);
8613 } else if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] < 2.0 {
8614 controller.push_basic_input(InputKind::Primary);
8615 } else {
8616 self.path_toward_target(
8617 agent,
8618 controller,
8619 tgt_data.pos.0,
8620 read_data,
8621 Path::Separate,
8622 None,
8623 );
8624 }
8625 } else {
8626 if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
8628 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8629 }
8630 }
8631 }
8632
8633 pub fn handle_clay_golem_attack(
8634 &self,
8635 agent: &mut Agent,
8636 controller: &mut Controller,
8637 attack_data: &AttackData,
8638 tgt_data: &TargetData,
8639 read_data: &ReadData,
8640 ) {
8641 const MIN_DASH_RANGE: f32 = 15.0;
8642
8643 enum ActionStateTimers {
8644 AttackTimer,
8645 }
8646
8647 let line_of_sight_with_target = || {
8648 entities_have_line_of_sight(
8649 self.pos,
8650 self.body,
8651 self.scale,
8652 tgt_data.pos,
8653 tgt_data.body,
8654 tgt_data.scale,
8655 read_data,
8656 )
8657 };
8658 let spawn = agent.patrol_origin.unwrap_or(self.pos.0);
8659 let home = Vec3::new(spawn.x - 32.0, spawn.y - 12.0, spawn.z);
8660 let is_home = (home - self.pos.0).xy().magnitude_squared() < (3.0_f32).powi(2);
8661 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8662 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
8663 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8665 }
8666 if line_of_sight_with_target() {
8667 controller.inputs.move_dir = Vec2::zero();
8668 if attack_data.in_min_range() {
8669 controller.push_basic_input(InputKind::Primary);
8670 } else if attack_data.dist_sqrd > MIN_DASH_RANGE.powi(2) {
8671 controller.push_basic_input(InputKind::Secondary);
8672 } else {
8673 self.path_toward_target(
8674 agent,
8675 controller,
8676 tgt_data.pos.0,
8677 read_data,
8678 Path::AtTarget,
8679 None,
8680 );
8681 }
8682 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.0 {
8683 if !is_home {
8684 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8686 } else {
8687 self.path_toward_target(agent, controller, spawn, read_data, Path::Separate, None);
8688 }
8689 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8690 self.path_toward_target(
8691 agent,
8692 controller,
8693 tgt_data.pos.0,
8694 read_data,
8695 Path::Separate,
8696 None,
8697 );
8698 }
8699 }
8700
8701 pub fn handle_haniwa_soldier(
8702 &self,
8703 agent: &mut Agent,
8704 controller: &mut Controller,
8705 attack_data: &AttackData,
8706 tgt_data: &TargetData,
8707 read_data: &ReadData,
8708 ) {
8709 const DEFENSIVE_CONDITION: usize = 0;
8710 const RIPOSTE_TIMER: usize = 0;
8711 const MODE_CYCLE_TIMER: usize = 1;
8712
8713 let primary = self.extract_ability(AbilityInput::Primary);
8714 let secondary = self.extract_ability(AbilityInput::Secondary);
8715 let could_use_input = |input| match input {
8716 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8717 p.could_use(
8718 attack_data,
8719 self,
8720 tgt_data,
8721 read_data,
8722 AbilityPreferences::default(),
8723 )
8724 }),
8725 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8726 s.could_use(
8727 attack_data,
8728 self,
8729 tgt_data,
8730 read_data,
8731 AbilityPreferences::default(),
8732 )
8733 }),
8734 _ => false,
8735 };
8736
8737 agent.combat_state.timers[RIPOSTE_TIMER] += read_data.dt.0;
8738 agent.combat_state.timers[MODE_CYCLE_TIMER] += read_data.dt.0;
8739
8740 if agent.combat_state.timers[MODE_CYCLE_TIMER] > 7.0 {
8741 agent.combat_state.conditions[DEFENSIVE_CONDITION] =
8742 !agent.combat_state.conditions[DEFENSIVE_CONDITION];
8743 agent.combat_state.timers[MODE_CYCLE_TIMER] = 0.0;
8744 }
8745
8746 if matches!(self.char_state, CharacterState::RiposteMelee(_)) {
8747 agent.combat_state.timers[RIPOSTE_TIMER] = 0.0;
8748 }
8749
8750 let try_move = if agent.combat_state.conditions[DEFENSIVE_CONDITION] {
8751 controller.push_basic_input(InputKind::Block);
8752 true
8753 } else if agent.combat_state.timers[RIPOSTE_TIMER] > 10.0
8754 && could_use_input(InputKind::Secondary)
8755 {
8756 controller.push_basic_input(InputKind::Secondary);
8757 false
8758 } else if could_use_input(InputKind::Primary) {
8759 controller.push_basic_input(InputKind::Primary);
8760 false
8761 } else {
8762 true
8763 };
8764
8765 if try_move && attack_data.dist_sqrd > 2_f32.powi(2) {
8766 self.path_toward_target(
8767 agent,
8768 controller,
8769 tgt_data.pos.0,
8770 read_data,
8771 Path::Separate,
8772 None,
8773 );
8774 }
8775 }
8776
8777 pub fn handle_haniwa_guard(
8778 &self,
8779 agent: &mut Agent,
8780 controller: &mut Controller,
8781 attack_data: &AttackData,
8782 tgt_data: &TargetData,
8783 read_data: &ReadData,
8784 rng: &mut impl RngExt,
8785 ) {
8786 const BACKPEDAL_DIST: f32 = 5.0;
8787 const ROTATE_CCW_CONDITION: usize = 0;
8788 const FLURRY_TIMER: usize = 0;
8789 const BACKPEDAL_TIMER: usize = 1;
8790 const SWITCH_ROTATE_TIMER: usize = 2;
8791 const SWITCH_ROTATE_COUNTER: usize = 0;
8792
8793 let primary = self.extract_ability(AbilityInput::Primary);
8794 let secondary = self.extract_ability(AbilityInput::Secondary);
8795 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8796 let could_use_input = |input| match input {
8797 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8798 p.could_use(
8799 attack_data,
8800 self,
8801 tgt_data,
8802 read_data,
8803 AbilityPreferences::default(),
8804 )
8805 }),
8806 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8807 s.could_use(
8808 attack_data,
8809 self,
8810 tgt_data,
8811 read_data,
8812 AbilityPreferences::default(),
8813 )
8814 }),
8815 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8816 a.could_use(
8817 attack_data,
8818 self,
8819 tgt_data,
8820 read_data,
8821 AbilityPreferences::default(),
8822 )
8823 }),
8824 _ => false,
8825 };
8826
8827 if !agent.combat_state.initialized {
8828 agent.combat_state.conditions[ROTATE_CCW_CONDITION] = rng.random_bool(0.5);
8829 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8830 agent.combat_state.initialized = true;
8831 }
8832
8833 let continue_flurry = match self.char_state {
8834 CharacterState::BasicMelee(_) => {
8835 agent.combat_state.timers[FLURRY_TIMER] += read_data.dt.0;
8836 false
8837 },
8838 CharacterState::RapidMelee(c) => {
8839 agent.combat_state.timers[FLURRY_TIMER] = 0.0;
8840 !matches!(c.stage_section, StageSection::Recover)
8841 },
8842 CharacterState::ComboMelee2(_) => {
8843 agent.combat_state.timers[BACKPEDAL_TIMER] = 0.0;
8844 false
8845 },
8846 _ => false,
8847 };
8848 agent.combat_state.timers[SWITCH_ROTATE_TIMER] += read_data.dt.0;
8849 agent.combat_state.timers[BACKPEDAL_TIMER] += read_data.dt.0;
8850
8851 if agent.combat_state.timers[SWITCH_ROTATE_TIMER]
8852 > agent.combat_state.counters[SWITCH_ROTATE_COUNTER]
8853 {
8854 agent.combat_state.conditions[ROTATE_CCW_CONDITION] =
8855 !agent.combat_state.conditions[ROTATE_CCW_CONDITION];
8856 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8857 }
8858
8859 let move_farther = attack_data.dist_sqrd < BACKPEDAL_DIST.powi(2);
8860 let move_closer = if continue_flurry && could_use_input(InputKind::Secondary) {
8861 controller.push_basic_input(InputKind::Secondary);
8862 false
8863 } else if agent.combat_state.timers[BACKPEDAL_TIMER] > 10.0
8864 && move_farther
8865 && could_use_input(InputKind::Ability(0))
8866 {
8867 controller.push_basic_input(InputKind::Ability(0));
8868 false
8869 } else if agent.combat_state.timers[FLURRY_TIMER] > 6.0
8870 && could_use_input(InputKind::Secondary)
8871 {
8872 controller.push_basic_input(InputKind::Secondary);
8873 false
8874 } else if could_use_input(InputKind::Primary) {
8875 controller.push_basic_input(InputKind::Primary);
8876 false
8877 } else {
8878 true
8879 };
8880
8881 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
8882 &*read_data.terrain,
8883 self.pos.0,
8884 self.vel.0,
8885 tgt_data.pos.0,
8886 TraversalConfig {
8887 min_tgt_dist: 1.25,
8888 ..self.traversal_config
8889 },
8890 &read_data.time,
8891 ) {
8892 self.unstuck_if(stuck, controller);
8893 if entities_have_line_of_sight(
8894 self.pos,
8895 self.body,
8896 self.scale,
8897 tgt_data.pos,
8898 tgt_data.body,
8899 tgt_data.scale,
8900 read_data,
8901 ) && attack_data.angle < 45.0
8902 {
8903 let angle = match (
8904 agent.combat_state.conditions[ROTATE_CCW_CONDITION],
8905 move_closer,
8906 move_farther,
8907 ) {
8908 (true, true, false) => rng.random_range(-1.5..-0.5),
8909 (true, false, true) => rng.random_range(-2.2..-1.7),
8910 (true, _, _) => rng.random_range(-1.7..-1.5),
8911 (false, true, false) => rng.random_range(0.5..1.5),
8912 (false, false, true) => rng.random_range(1.7..2.2),
8913 (false, _, _) => rng.random_range(1.5..1.7),
8914 };
8915 controller.inputs.move_dir = bearing
8916 .xy()
8917 .rotated_z(angle)
8918 .try_normalized()
8919 .unwrap_or_else(Vec2::zero)
8920 * speed;
8921 } else {
8922 controller.inputs.move_dir =
8923 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
8924 self.jump_if(bearing.z > 1.5, controller);
8925 }
8926 }
8927 }
8928
8929 pub fn handle_haniwa_archer(
8930 &self,
8931 agent: &mut Agent,
8932 controller: &mut Controller,
8933 attack_data: &AttackData,
8934 tgt_data: &TargetData,
8935 read_data: &ReadData,
8936 ) {
8937 const KICK_TIMER: usize = 0;
8938 const EXPLOSIVE_TIMER: usize = 1;
8939
8940 let primary = self.extract_ability(AbilityInput::Primary);
8941 let secondary = self.extract_ability(AbilityInput::Secondary);
8942 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8943 let could_use_input = |input| match input {
8944 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8945 p.could_use(
8946 attack_data,
8947 self,
8948 tgt_data,
8949 read_data,
8950 AbilityPreferences::default(),
8951 )
8952 }),
8953 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8954 s.could_use(
8955 attack_data,
8956 self,
8957 tgt_data,
8958 read_data,
8959 AbilityPreferences::default(),
8960 )
8961 }),
8962 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8963 a.could_use(
8964 attack_data,
8965 self,
8966 tgt_data,
8967 read_data,
8968 AbilityPreferences::default(),
8969 )
8970 }),
8971 _ => false,
8972 };
8973
8974 agent.combat_state.timers[KICK_TIMER] += read_data.dt.0;
8975 agent.combat_state.timers[EXPLOSIVE_TIMER] += read_data.dt.0;
8976
8977 match self.char_state.ability_info().map(|ai| ai.input) {
8978 Some(InputKind::Secondary) => {
8979 agent.combat_state.timers[KICK_TIMER] = 0.0;
8980 },
8981 Some(InputKind::Ability(0)) => {
8982 agent.combat_state.timers[EXPLOSIVE_TIMER] = 0.0;
8983 },
8984 _ => {},
8985 }
8986
8987 if agent.combat_state.timers[KICK_TIMER] > 4.0 && could_use_input(InputKind::Secondary) {
8988 controller.push_basic_input(InputKind::Secondary);
8989 } else if agent.combat_state.timers[EXPLOSIVE_TIMER] > 15.0
8990 && could_use_input(InputKind::Ability(0))
8991 {
8992 controller.push_basic_input(InputKind::Ability(0));
8993 } else if could_use_input(InputKind::Primary) {
8994 controller.push_basic_input(InputKind::Primary);
8995 } else {
8996 self.path_toward_target(
8997 agent,
8998 controller,
8999 tgt_data.pos.0,
9000 read_data,
9001 Path::Separate,
9002 None,
9003 );
9004 }
9005 }
9006
9007 pub fn handle_terracotta_statue_attack(
9008 &self,
9009 agent: &mut Agent,
9010 controller: &mut Controller,
9011 attack_data: &AttackData,
9012 read_data: &ReadData,
9013 ) {
9014 enum Conditions {
9015 AttackToggle,
9016 }
9017 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
9018 if (home - self.pos.0).xy().magnitude_squared() > (2.0_f32).powi(2) {
9020 self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
9021 } else if !agent.combat_state.conditions[Conditions::AttackToggle as usize] {
9022 controller.push_basic_input(InputKind::Primary);
9024 } else {
9025 controller.inputs.move_dir = Vec2::zero();
9026 if attack_data.dist_sqrd < 8.5f32.powi(2) {
9027 controller.push_basic_input(InputKind::Primary);
9029 } else {
9030 controller.push_basic_input(InputKind::Secondary);
9032 }
9033 }
9034 if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
9035 {
9036 agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
9037 }
9038 }
9039
9040 pub fn handle_jiangshi_attack(
9041 &self,
9042 agent: &mut Agent,
9043 controller: &mut Controller,
9044 attack_data: &AttackData,
9045 tgt_data: &TargetData,
9046 read_data: &ReadData,
9047 ) {
9048 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
9049 controller.push_action(ControlAction::StartInput {
9050 input: InputKind::Secondary,
9051 target_entity: agent
9052 .target
9053 .as_ref()
9054 .and_then(|t| read_data.uids.get(t.target))
9055 .copied(),
9056 select_pos: None,
9057 });
9058 } else if attack_data.dist_sqrd < 12.0f32.powi(2) {
9059 controller.push_basic_input(InputKind::Primary);
9060 }
9061
9062 self.path_toward_target(
9063 agent,
9064 controller,
9065 tgt_data.pos.0,
9066 read_data,
9067 Path::AtTarget,
9068 None,
9069 );
9070 }
9071}