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