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