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