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_rocksnapper_attack(
4938 &self,
4939 agent: &mut Agent,
4940 controller: &mut Controller,
4941 attack_data: &AttackData,
4942 tgt_data: &TargetData,
4943 read_data: &ReadData,
4944 ) {
4945 const LEAP_TIMER: f32 = 3.0;
4946 const DASH_TIMER: f32 = 5.0;
4947 const LEAP_RANGE: f32 = 20.0;
4948 const MELEE_RANGE: f32 = 5.0;
4949
4950 enum ActionStateTimers {
4951 TimerRocksnapperDash = 0,
4952 TimerRocksnapperLeap = 1,
4953 }
4954 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] +=
4955 read_data.dt.0;
4956 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] +=
4957 read_data.dt.0;
4958
4959 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
4960 {
4961 controller.push_basic_input(InputKind::Secondary);
4963 } else if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize]
4964 > DASH_TIMER
4965 {
4966 controller.push_basic_input(InputKind::Secondary);
4968
4969 if matches!(self.char_state, CharacterState::DashMelee(_)) {
4970 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperDash as usize] = 0.0;
4972 }
4973 } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) && attack_data.angle < 90.0 {
4974 if agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize]
4975 > LEAP_TIMER
4976 {
4977 controller.push_basic_input(InputKind::Ability(0));
4979
4980 if matches!(self.char_state, CharacterState::LeapShockwave(_)) {
4981 agent.combat_state.timers[ActionStateTimers::TimerRocksnapperLeap as usize] =
4983 0.0;
4984 }
4985 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
4986 controller.push_basic_input(InputKind::Primary);
4988 }
4989 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
4990 controller.push_basic_input(InputKind::Primary);
4992 }
4993
4994 self.path_toward_target(
4996 agent,
4997 controller,
4998 tgt_data.pos.0,
4999 read_data,
5000 Path::AtTarget,
5001 None,
5002 );
5003 }
5004
5005 pub fn handle_roshwalr_attack(
5006 &self,
5007 agent: &mut Agent,
5008 controller: &mut Controller,
5009 attack_data: &AttackData,
5010 tgt_data: &TargetData,
5011 read_data: &ReadData,
5012 ) {
5013 const SLOW_CHARGE_RANGE: f32 = 12.5;
5014 const SHOCKWAVE_RANGE: f32 = 12.5;
5015 const SHOCKWAVE_TIMER: f32 = 15.0;
5016 const MELEE_RANGE: f32 = 4.0;
5017
5018 enum ActionStateFCounters {
5019 FCounterRoshwalrAttack = 0,
5020 }
5021
5022 agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize] +=
5023 read_data.dt.0;
5024 if matches!(self.char_state, CharacterState::DashMelee(c) if !matches!(c.stage_section, StageSection::Recover))
5025 {
5026 controller.push_basic_input(InputKind::Ability(0));
5028 } else if attack_data.dist_sqrd < SHOCKWAVE_RANGE.powi(2) && attack_data.angle < 270.0 {
5029 if agent.combat_state.counters[ActionStateFCounters::FCounterRoshwalrAttack as usize]
5030 > SHOCKWAVE_TIMER
5031 {
5032 controller.push_basic_input(InputKind::Ability(0));
5034
5035 if matches!(self.char_state, CharacterState::Shockwave(_)) {
5036 agent.combat_state.counters
5038 [ActionStateFCounters::FCounterRoshwalrAttack as usize] = 0.0;
5039 }
5040 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) && attack_data.angle < 135.0 {
5041 controller.push_basic_input(InputKind::Primary);
5043 }
5044 } else if attack_data.dist_sqrd > SLOW_CHARGE_RANGE.powi(2) {
5045 controller.push_basic_input(InputKind::Secondary);
5047 }
5048
5049 self.path_toward_target(
5051 agent,
5052 controller,
5053 tgt_data.pos.0,
5054 read_data,
5055 Path::AtTarget,
5056 None,
5057 );
5058 }
5059
5060 pub fn handle_harvester_attack(
5061 &self,
5062 agent: &mut Agent,
5063 controller: &mut Controller,
5064 attack_data: &AttackData,
5065 tgt_data: &TargetData,
5066 read_data: &ReadData,
5067 rng: &mut impl Rng,
5068 ) {
5069 const FIRST_VINE_CREATION_THRESHOLD: f32 = 0.60;
5083 const SECOND_VINE_CREATION_THRESHOLD: f32 = 0.30;
5084 const PATH_RANGE_FACTOR: f32 = 0.4; const SCYTHE_RANGE_FACTOR: f32 = 0.75; const SCYTHE_AIM_FACTOR: f32 = 0.7;
5087 const FIREBREATH_RANGE_FACTOR: f32 = 0.7;
5088 const FIREBREATH_AIM_FACTOR: f32 = 0.8;
5089 const FIREBREATH_TIME_LIMIT: f32 = 4.0;
5090 const FIREBREATH_SHORT_TIME_LIMIT: f32 = 2.5; const FIREBREATH_COOLDOWN: f32 = 3.5;
5092 const PUMPKIN_RANGE_FACTOR: f32 = 0.75;
5093 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;
5099 const HAS_SUMMONED_SECOND_VINES: usize = 1;
5100 const FIREBREATH: usize = 0;
5102 const MIXUP: usize = 1;
5103 const FAR_PUMPKIN: usize = 2;
5104 const CLOSE_MIXUP_COOLDOWN: usize = 0;
5106 const MID_MIXUP_COOLDOWN: usize = 1;
5107 const FAR_PUMPKIN_COOLDOWN: usize = 2;
5108
5109 let line_of_sight_with_target = || {
5111 entities_have_line_of_sight(
5112 self.pos,
5113 self.body,
5114 self.scale,
5115 tgt_data.pos,
5116 tgt_data.body,
5117 tgt_data.scale,
5118 read_data,
5119 )
5120 };
5121
5122 let (scythe_range, scythe_angle) = {
5125 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
5126 self.extract_ability(AbilityInput::Primary)
5127 {
5128 (range, angle)
5129 } else {
5130 (0.0, 0.0)
5131 }
5132 };
5133 let (firebreath_range, firebreath_angle) = {
5134 if let Some(AbilityData::BasicBeam { range, angle, .. }) =
5135 self.extract_ability(AbilityInput::Secondary)
5136 {
5137 (range, angle)
5138 } else {
5139 (0.0, 0.0)
5140 }
5141 };
5142 let pumpkin_speed = {
5143 if let Some(AbilityData::BasicRanged {
5144 projectile_speed, ..
5145 }) = self.extract_ability(AbilityInput::Auxiliary(0))
5146 {
5147 projectile_speed
5148 } else {
5149 0.0
5150 }
5151 };
5152 let pumpkin_max_range =
5154 projectile_flat_range(pumpkin_speed, self.body.map_or(0.0, |b| b.height()));
5155
5156 let is_using_firebreath = matches!(self.char_state, CharacterState::BasicBeam(_));
5158 let is_using_pumpkin = matches!(self.char_state, CharacterState::BasicRanged(_));
5159 let is_in_summon_recovery = matches!(self.char_state, CharacterState::SpriteSummon(data) if matches!(data.stage_section, StageSection::Recover));
5160 let firebreath_timer = if let CharacterState::BasicBeam(data) = self.char_state {
5161 data.timer
5162 } else {
5163 Default::default()
5164 };
5165 let is_using_mixup = is_using_firebreath || is_using_pumpkin;
5166
5167 if !agent.combat_state.initialized {
5169 agent.combat_state.initialized = true;
5170 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5171 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5172 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5173 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5174 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5175 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5176 }
5177
5178 if is_in_summon_recovery {
5182 agent.combat_state.timers[FIREBREATH] = 0.0;
5184 agent.combat_state.timers[MIXUP] = 0.0;
5185 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5186 } else {
5187 if is_using_firebreath {
5189 agent.combat_state.timers[FIREBREATH] = 0.0;
5190 } else {
5191 agent.combat_state.timers[FIREBREATH] += read_data.dt.0;
5192 }
5193 if is_using_mixup {
5194 agent.combat_state.timers[MIXUP] = 0.0;
5195 } else {
5196 agent.combat_state.timers[MIXUP] += read_data.dt.0;
5197 }
5198 if is_using_pumpkin {
5199 agent.combat_state.timers[FAR_PUMPKIN] = 0.0;
5200 } else {
5201 agent.combat_state.timers[FAR_PUMPKIN] += read_data.dt.0;
5202 }
5203 }
5204
5205 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5207 if health_fraction < SECOND_VINE_CREATION_THRESHOLD
5209 && !agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES]
5210 {
5211 controller.push_basic_input(InputKind::Ability(2));
5213 if is_in_summon_recovery {
5215 agent.combat_state.conditions[HAS_SUMMONED_SECOND_VINES] = true;
5216 }
5217 }
5218 else if health_fraction < FIRST_VINE_CREATION_THRESHOLD
5220 && !agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES]
5221 {
5222 controller.push_basic_input(InputKind::Ability(1));
5224 if is_in_summon_recovery {
5226 agent.combat_state.conditions[HAS_SUMMONED_FIRST_VINES] = true;
5227 }
5228 }
5229 else if attack_data.dist_sqrd
5231 < (attack_data.body_dist + scythe_range * SCYTHE_RANGE_FACTOR).powi(2)
5232 {
5233 if is_using_firebreath
5235 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_SHORT_TIME_LIMIT)
5236 {
5237 controller.push_basic_input(InputKind::Secondary);
5238 }
5239 if attack_data.angle < scythe_angle * SCYTHE_AIM_FACTOR {
5241 if agent.combat_state.timers[MIXUP]
5243 > agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN]
5244 {
5246 if agent.combat_state.timers[FIREBREATH] < FIREBREATH_COOLDOWN {
5248 controller.push_basic_input(InputKind::Ability(0));
5249 }
5250 else if rng.random_bool(0.5) {
5252 controller.push_basic_input(InputKind::Secondary);
5253 } else {
5254 controller.push_basic_input(InputKind::Ability(0));
5255 }
5256 if is_using_mixup {
5258 agent.combat_state.counters[CLOSE_MIXUP_COOLDOWN] =
5259 rng_from_span(rng, CLOSE_MIXUP_COOLDOWN_SPAN);
5260 }
5261 }
5262 else {
5264 controller.push_basic_input(InputKind::Primary);
5265 }
5266 }
5267 } else if attack_data.dist_sqrd < firebreath_range.powi(2) {
5269 #[expect(clippy::if_same_then_else)]
5271 if is_using_firebreath
5272 && firebreath_timer < Duration::from_secs_f32(FIREBREATH_TIME_LIMIT)
5273 {
5274 controller.push_basic_input(InputKind::Secondary);
5275 }
5276 else if attack_data.dist_sqrd < (firebreath_range * FIREBREATH_RANGE_FACTOR).powi(2)
5278 && attack_data.angle < firebreath_angle * FIREBREATH_AIM_FACTOR
5279 && agent.combat_state.timers[FIREBREATH] > FIREBREATH_COOLDOWN
5280 {
5281 controller.push_basic_input(InputKind::Secondary);
5282 }
5283 else if agent.combat_state.timers[MIXUP]
5285 > agent.combat_state.counters[MID_MIXUP_COOLDOWN]
5286 {
5287 controller.push_basic_input(InputKind::Ability(0));
5288 if is_using_pumpkin {
5290 agent.combat_state.counters[MID_MIXUP_COOLDOWN] =
5291 rng_from_span(rng, MID_MIXUP_COOLDOWN_SPAN);
5292 }
5293 }
5294 }
5295 else if attack_data.dist_sqrd < (pumpkin_max_range * PUMPKIN_RANGE_FACTOR).powi(2)
5297 && agent.combat_state.timers[FAR_PUMPKIN]
5298 > agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN]
5299 && line_of_sight_with_target()
5300 {
5301 controller.push_basic_input(InputKind::Ability(0));
5303 if is_using_pumpkin {
5305 agent.combat_state.counters[FAR_PUMPKIN_COOLDOWN] =
5306 rng_from_span(rng, FAR_PUMPKIN_COOLDOWN_SPAN);
5307 }
5308 }
5309
5310 if attack_data.dist_sqrd
5313 > (attack_data.body_dist + scythe_range * PATH_RANGE_FACTOR).powi(2)
5314 {
5315 self.path_toward_target(
5316 agent,
5317 controller,
5318 tgt_data.pos.0,
5319 read_data,
5320 Path::AtTarget,
5321 None,
5322 );
5323 }
5324 else if attack_data.angle > 0.0 {
5326 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
5328 .xy()
5329 .try_normalized()
5330 .unwrap_or_else(Vec2::zero)
5331 * 0.001; }
5333 }
5334
5335 pub fn handle_frostgigas_attack(
5336 &self,
5337 agent: &mut Agent,
5338 controller: &mut Controller,
5339 attack_data: &AttackData,
5340 tgt_data: &TargetData,
5341 read_data: &ReadData,
5342 rng: &mut impl Rng,
5343 ) {
5344 const GIGAS_MELEE_RANGE: f32 = 12.0;
5345 const GIGAS_SPIKE_RANGE: f32 = 16.0;
5346 const ICEBOMB_RANGE: f32 = 70.0;
5347 const GIGAS_LEAP_RANGE: f32 = 50.0;
5348 const MINION_SUMMON_THRESHOLD: f32 = 1. / 8.;
5349 const FLASHFREEZE_RANGE: f32 = 30.;
5350
5351 enum ActionStateTimers {
5352 AttackChange,
5353 Bonk,
5354 }
5355
5356 enum ActionStateFCounters {
5357 FCounterMinionSummonThreshold = 0,
5358 }
5359
5360 enum ActionStateICounters {
5361 CurrentAbility = 0,
5366 }
5367
5368 let should_use_targeted_spikes = || matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { depth, .. }) if depth >= 2.0);
5369 let remote_spikes_action = || ControlAction::StartInput {
5370 input: InputKind::Ability(5),
5371 target_entity: None,
5372 select_pos: Some(tgt_data.pos.0),
5373 };
5374
5375 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5376 if !agent.combat_state.initialized {
5379 agent.combat_state.counters
5380 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5381 1.0 - MINION_SUMMON_THRESHOLD;
5382 agent.combat_state.initialized = true;
5383 }
5384
5385 if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 6.0 {
5387 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] = 0.0;
5388 } else {
5389 agent.combat_state.timers[ActionStateTimers::AttackChange as usize] += read_data.dt.0;
5390 }
5391 agent.combat_state.timers[ActionStateTimers::Bonk as usize] += read_data.dt.0;
5392
5393 if health_fraction
5394 < agent.combat_state.counters
5395 [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5396 {
5397 controller.push_basic_input(InputKind::Ability(3));
5399
5400 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5401 {
5402 agent.combat_state.counters
5403 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5404 MINION_SUMMON_THRESHOLD;
5405 }
5406 } else if let Some(ability) = Some(
5408 &mut agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize],
5409 )
5410 .filter(|i| **i != 0)
5411 {
5412 if *ability == 3 && should_use_targeted_spikes() {
5413 *ability = 5
5414 };
5415
5416 let reset = match ability {
5417 1 => {
5419 controller.push_basic_input(InputKind::Ability(1));
5420 matches!(self.char_state, CharacterState::LeapShockwave(c) if matches!(c.stage_section, StageSection::Recover))
5421 },
5422 2 => {
5424 controller.push_basic_input(InputKind::Ability(4));
5425 matches!(self.char_state, CharacterState::Shockwave(c) if matches!(c.stage_section, StageSection::Recover))
5426 },
5427 3 => {
5429 controller.push_basic_input(InputKind::Ability(0));
5430 matches!(self.char_state, CharacterState::SpriteSummon(c)
5431 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Summoner)))
5432 },
5433 4 => {
5435 controller.push_basic_input(InputKind::Ability(7));
5436 matches!(self.char_state, CharacterState::RapidMelee(c) if matches!(c.stage_section, StageSection::Recover))
5437 },
5438 5 => {
5440 controller.push_action(remote_spikes_action());
5441 matches!(self.char_state, CharacterState::SpriteSummon(c)
5442 if matches!((c.stage_section, c.static_data.anchor), (StageSection::Recover, SpriteSummonAnchor::Target)))
5443 },
5444 6 => {
5446 controller.push_basic_input(InputKind::Ability(2));
5447 matches!(self.char_state, CharacterState::BasicRanged(c) if matches!(c.stage_section, StageSection::Recover))
5448 },
5449 _ => true,
5451 };
5452
5453 if reset {
5454 *ability = 0;
5455 }
5456 } else if attack_data.dist_sqrd > 5f32.powi(2)
5460 && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6
5462 && rng.random_bool((0.2 * read_data.dt.0).min(1.0) as f64)
5464 {
5465 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5466 rng.random_range(5..=6);
5467 } else if attack_data.dist_sqrd < GIGAS_MELEE_RANGE.powi(2) {
5468 if agent.combat_state.timers[ActionStateTimers::Bonk as usize] > 10. {
5470 controller.push_basic_input(InputKind::Ability(6));
5471
5472 if matches!(self.char_state, CharacterState::BasicMelee(c)
5473 if matches!(c.stage_section, StageSection::Recover) &&
5474 c.static_data.ability_info.ability.is_some_and(|meta| matches!(meta.ability, Ability::MainWeaponAux(6)))
5475 ) {
5476 agent.combat_state.timers[ActionStateTimers::Bonk as usize] =
5477 rng.random_range(0.0..3.0);
5478 }
5479 } else if agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 4.0
5481 && rng.random_bool(0.1 * read_data.dt.0.min(1.0) as f64)
5482 {
5483 agent.combat_state.int_counters[ActionStateICounters::CurrentAbility as usize] =
5484 rng.random_range(1..=4);
5485 } else if attack_data.angle > 90.0
5488 || agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 5.0
5489 {
5490 if attack_data.angle > 120.0 {
5492 agent.combat_state.int_counters
5493 [ActionStateICounters::CurrentAbility as usize] = 4;
5494 } else {
5495 controller.push_basic_input(InputKind::Secondary);
5496 }
5497 } else {
5498 controller.push_basic_input(InputKind::Primary);
5499 }
5500 } else if attack_data.dist_sqrd < GIGAS_SPIKE_RANGE.powi(2)
5501 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 2.0
5502 {
5503 if should_use_targeted_spikes() {
5504 controller.push_action(remote_spikes_action());
5505 } else {
5506 controller.push_basic_input(InputKind::Ability(0));
5507 }
5508 } else if attack_data.dist_sqrd < FLASHFREEZE_RANGE.powi(2)
5509 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 4.0
5510 {
5511 controller.push_basic_input(InputKind::Ability(4));
5512 } else if attack_data.dist_sqrd < GIGAS_LEAP_RANGE.powi(2)
5514 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] > 3.0
5515 {
5516 controller.push_basic_input(InputKind::Ability(1));
5517 } else if attack_data.dist_sqrd < ICEBOMB_RANGE.powi(2)
5518 && agent.combat_state.timers[ActionStateTimers::AttackChange as usize] < 3.0
5519 {
5520 controller.push_basic_input(InputKind::Ability(2));
5521 } else {
5523 controller.push_action(remote_spikes_action());
5524 }
5525
5526 self.path_toward_target(
5528 agent,
5529 controller,
5530 tgt_data.pos.0,
5531 read_data,
5532 Path::AtTarget,
5533 attack_data.in_min_range().then_some(0.1),
5534 );
5535 }
5536
5537 pub fn handle_boreal_hammer_attack(
5538 &self,
5539 agent: &mut Agent,
5540 controller: &mut Controller,
5541 attack_data: &AttackData,
5542 tgt_data: &TargetData,
5543 read_data: &ReadData,
5544 rng: &mut impl Rng,
5545 ) {
5546 enum ActionStateTimers {
5547 TimerHandleHammerAttack = 0,
5548 }
5549
5550 let has_energy = |need| self.energy.current() > need;
5551
5552 let use_leap = |controller: &mut Controller| {
5553 controller.push_basic_input(InputKind::Ability(0));
5554 };
5555
5556 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] +=
5557 read_data.dt.0;
5558
5559 if attack_data.in_min_range() && attack_data.angle < 45.0 {
5560 controller.inputs.move_dir = Vec2::zero();
5561 if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] > 4.0
5562 {
5563 controller.push_cancel_input(InputKind::Secondary);
5564 agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize] =
5565 0.0;
5566 } else if agent.combat_state.timers[ActionStateTimers::TimerHandleHammerAttack as usize]
5567 > 3.0
5568 {
5569 controller.push_basic_input(InputKind::Secondary);
5570 } else if has_energy(50.0) && rng.random_bool(0.9) {
5571 use_leap(controller);
5572 } else {
5573 controller.push_basic_input(InputKind::Primary);
5574 }
5575 } else {
5576 self.path_toward_target(
5577 agent,
5578 controller,
5579 tgt_data.pos.0,
5580 read_data,
5581 Path::Separate,
5582 None,
5583 );
5584
5585 if attack_data.dist_sqrd < 32.0f32.powi(2)
5586 && entities_have_line_of_sight(
5587 self.pos,
5588 self.body,
5589 self.scale,
5590 tgt_data.pos,
5591 tgt_data.body,
5592 tgt_data.scale,
5593 read_data,
5594 )
5595 {
5596 if rng.random_bool(0.5) && has_energy(50.0) {
5597 use_leap(controller);
5598 } else if agent.combat_state.timers
5599 [ActionStateTimers::TimerHandleHammerAttack as usize]
5600 > 2.0
5601 {
5602 controller.push_basic_input(InputKind::Secondary);
5603 } else if agent.combat_state.timers
5604 [ActionStateTimers::TimerHandleHammerAttack as usize]
5605 > 4.0
5606 {
5607 controller.push_cancel_input(InputKind::Secondary);
5608 agent.combat_state.timers
5609 [ActionStateTimers::TimerHandleHammerAttack as usize] = 0.0;
5610 }
5611 }
5612 }
5613 }
5614
5615 pub fn handle_boreal_bow_attack(
5616 &self,
5617 agent: &mut Agent,
5618 controller: &mut Controller,
5619 attack_data: &AttackData,
5620 tgt_data: &TargetData,
5621 read_data: &ReadData,
5622 rng: &mut impl Rng,
5623 ) {
5624 let line_of_sight_with_target = || {
5625 entities_have_line_of_sight(
5626 self.pos,
5627 self.body,
5628 self.scale,
5629 tgt_data.pos,
5630 tgt_data.body,
5631 tgt_data.scale,
5632 read_data,
5633 )
5634 };
5635
5636 let has_energy = |need| self.energy.current() > need;
5637
5638 let use_trap = |controller: &mut Controller| {
5639 controller.push_basic_input(InputKind::Ability(0));
5640 };
5641
5642 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
5643 if rng.random_bool(0.5) && has_energy(15.0) {
5644 controller.push_basic_input(InputKind::Secondary);
5645 } else if attack_data.angle < 15.0 {
5646 controller.push_basic_input(InputKind::Primary);
5647 }
5648 } else if attack_data.dist_sqrd < (4.0 * attack_data.min_attack_dist).powi(2)
5649 && line_of_sight_with_target()
5650 {
5651 if rng.random_bool(0.5) && has_energy(15.0) {
5652 controller.push_basic_input(InputKind::Secondary);
5653 } else if has_energy(20.0) {
5654 use_trap(controller);
5655 }
5656 }
5657
5658 if has_energy(50.0) {
5659 if attack_data.dist_sqrd < (10.0 * attack_data.min_attack_dist).powi(2) {
5660 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
5662 &*read_data.terrain,
5663 self.pos.0,
5664 self.vel.0,
5665 tgt_data.pos.0,
5666 TraversalConfig {
5667 min_tgt_dist: 1.25,
5668 ..self.traversal_config
5669 },
5670 &read_data.time,
5671 ) {
5672 self.unstuck_if(stuck, controller);
5673 if line_of_sight_with_target() && attack_data.angle < 45.0 {
5674 controller.inputs.move_dir = bearing
5675 .xy()
5676 .rotated_z(rng.random_range(0.5..1.57))
5677 .try_normalized()
5678 .unwrap_or_else(Vec2::zero)
5679 * 2.0
5680 * speed;
5681 } else {
5682 controller.inputs.move_dir =
5684 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
5685 self.jump_if(bearing.z > 1.5, controller);
5686 controller.inputs.move_z = bearing.z;
5687 }
5688 }
5689 } else {
5690 self.path_toward_target(
5692 agent,
5693 controller,
5694 tgt_data.pos.0,
5695 read_data,
5696 Path::AtTarget,
5697 None,
5698 );
5699 }
5700 } else {
5701 self.path_toward_target(
5703 agent,
5704 controller,
5705 tgt_data.pos.0,
5706 read_data,
5707 Path::AtTarget,
5708 None,
5709 );
5710 }
5711 }
5712
5713 pub fn handle_firegigas_attack(
5714 &self,
5715 agent: &mut Agent,
5716 controller: &mut Controller,
5717 attack_data: &AttackData,
5718 tgt_data: &TargetData,
5719 read_data: &ReadData,
5720 rng: &mut impl Rng,
5721 ) {
5722 const MELEE_RANGE: f32 = 12.0;
5723 const RANGED_RANGE: f32 = 27.0;
5724 const LEAP_RANGE: f32 = 50.0;
5725 const MINION_SUMMON_THRESHOLD: f32 = 1.0 / 8.0;
5726 const OVERHEAT_DUR: f32 = 3.0;
5727 const FORCE_GAP_CLOSER_TIMEOUT: f32 = 10.0;
5728
5729 enum ActionStateTimers {
5730 Special,
5731 Overheat,
5732 OutOfMeleeRange,
5733 }
5734
5735 enum ActionStateFCounters {
5736 FCounterMinionSummonThreshold,
5737 }
5738
5739 enum ActionStateConditions {
5740 VerticalStrikeCombo,
5741 WhirlwindTwice,
5742 }
5743
5744 const FAST_SLASH: InputKind = InputKind::Primary;
5745 const FAST_THRUST: InputKind = InputKind::Secondary;
5746 const SLOW_SLASH: InputKind = InputKind::Ability(0);
5747 const SLOW_THRUST: InputKind = InputKind::Ability(1);
5748 const LAVA_LEAP: InputKind = InputKind::Ability(2);
5749 const VERTICAL_STRIKE: InputKind = InputKind::Ability(3);
5750 const OVERHEAT: InputKind = InputKind::Ability(4);
5751 const WHIRLWIND: InputKind = InputKind::Ability(5);
5752 const EXPLOSIVE_STRIKE: InputKind = InputKind::Ability(6);
5753 const FIRE_PILLARS: InputKind = InputKind::Ability(7);
5754 const TARGETED_FIRE_PILLAR: InputKind = InputKind::Ability(8);
5755 const ASHEN_SUMMONS: InputKind = InputKind::Ability(9);
5756 const PARRY_PUNISH: InputKind = InputKind::Ability(10);
5757
5758 fn choose_weighted<const N: usize>(
5759 rng: &mut impl Rng,
5760 choices: [(InputKind, f32); N],
5761 ) -> InputKind {
5762 choices
5763 .choose_weighted(rng, |(_, weight)| *weight)
5764 .expect("weights should be valid")
5765 .0
5766 }
5767
5768 fn rand_basic(rng: &mut impl Rng, damage_fraction: f32) -> InputKind {
5770 choose_weighted(rng, [
5771 (FAST_SLASH, 2.0),
5772 (FAST_THRUST, 2.0),
5773 (SLOW_SLASH, 1.0 + damage_fraction),
5774 (SLOW_THRUST, 1.0 + damage_fraction),
5775 ])
5776 }
5777
5778 fn rand_special(rng: &mut impl Rng) -> InputKind {
5780 choose_weighted(rng, [
5781 (WHIRLWIND, 6.0),
5782 (VERTICAL_STRIKE, 6.0),
5783 (OVERHEAT, 6.0),
5784 (EXPLOSIVE_STRIKE, 1.0),
5785 (LAVA_LEAP, 1.0),
5786 (FIRE_PILLARS, 1.0),
5787 ])
5788 }
5789
5790 fn rand_aoe(rng: &mut impl Rng) -> InputKind {
5792 choose_weighted(rng, [
5793 (EXPLOSIVE_STRIKE, 1.0),
5794 (FIRE_PILLARS, 1.0),
5795 (WHIRLWIND, 2.0),
5796 ])
5797 }
5798
5799 fn rand_ranged(rng: &mut impl Rng) -> InputKind {
5801 choose_weighted(rng, [
5802 (EXPLOSIVE_STRIKE, 1.0),
5803 (FIRE_PILLARS, 1.0),
5804 (OVERHEAT, 1.0),
5805 ])
5806 }
5807
5808 let cast_targeted_fire_pillar = |c: &mut Controller| {
5809 c.push_action(ControlAction::StartInput {
5810 input: TARGETED_FIRE_PILLAR,
5811 target_entity: tgt_data.uid,
5812 select_pos: None,
5813 })
5814 };
5815
5816 fn can_cast_new_ability(char_state: &CharacterState) -> bool {
5817 !matches!(
5818 char_state,
5819 CharacterState::LeapMelee(_)
5820 | CharacterState::BasicMelee(_)
5821 | CharacterState::BasicBeam(_)
5822 | CharacterState::BasicSummon(_)
5823 | CharacterState::SpriteSummon(_)
5824 )
5825 }
5826
5827 if !agent.combat_state.initialized {
5829 agent.combat_state.counters
5830 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] =
5831 1.0 - MINION_SUMMON_THRESHOLD;
5832 agent.combat_state.initialized = true;
5833 }
5834
5835 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
5836 let damage_fraction = 1.0 - health_fraction;
5837 let cheesed_from_above = !agent.combat_state.conditions
5841 [ActionStateConditions::VerticalStrikeCombo as usize]
5842 && attack_data.dist_sqrd > 5f32.powi(2)
5843 && (tgt_data.pos.0 - self.pos.0).normalized().map(f32::abs).z > 0.6;
5844 let cheesed_in_water = matches!(self.physics_state.in_fluid, Some(Fluid::Liquid { kind: LiquidKind::Water, depth, .. }) if depth >= 2.0);
5846 let cheesed = cheesed_from_above || cheesed_in_water;
5847 let tgt_airborne = tgt_data
5848 .physics_state
5849 .is_some_and(|physics| physics.on_ground.is_none() && physics.in_liquid().is_none());
5850 let tgt_missed_parry = match tgt_data.char_state {
5851 Some(CharacterState::RiposteMelee(data)) => {
5852 matches!(data.stage_section, StageSection::Recover) && data.whiffed
5853 },
5854 Some(CharacterState::BasicBlock(data)) => {
5855 matches!(data.stage_section, StageSection::Recover)
5856 && !data.static_data.parry_window.recover
5857 && !data.is_parry
5858 },
5859 _ => false,
5860 };
5861 let casting_beam = matches!(self.char_state, CharacterState::BasicBeam(_))
5862 && self.char_state.stage_section() != Some(StageSection::Recover);
5863
5864 agent.combat_state.timers[ActionStateTimers::Special as usize] += read_data.dt.0;
5866 if casting_beam {
5867 agent.combat_state.timers[ActionStateTimers::Overheat as usize] += read_data.dt.0;
5868 } else {
5869 agent.combat_state.timers[ActionStateTimers::Overheat as usize] = 0.0;
5870 }
5871 if attack_data.dist_sqrd > MELEE_RANGE.powi(2) {
5872 agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] +=
5873 read_data.dt.0;
5874 } else {
5875 agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize] = 0.0;
5876 }
5877
5878 if casting_beam
5880 && agent.combat_state.timers[ActionStateTimers::Overheat as usize] < OVERHEAT_DUR
5881 {
5882 controller.push_basic_input(OVERHEAT);
5883 controller.inputs.look_dir = self
5884 .ori
5885 .look_dir()
5886 .to_horizontal()
5887 .unwrap_or_else(|| self.ori.look_dir());
5888 } else if health_fraction
5889 < agent.combat_state.counters
5890 [ActionStateFCounters::FCounterMinionSummonThreshold as usize]
5891 {
5892 controller.push_basic_input(ASHEN_SUMMONS);
5894
5895 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
5896 {
5897 agent.combat_state.counters
5898 [ActionStateFCounters::FCounterMinionSummonThreshold as usize] -=
5899 MINION_SUMMON_THRESHOLD;
5900 }
5901 } else if can_cast_new_ability(self.char_state) {
5902 if cheesed {
5903 cast_targeted_fire_pillar(controller);
5904 } else if agent.combat_state.conditions
5905 [ActionStateConditions::VerticalStrikeCombo as usize]
5906 {
5907 if tgt_airborne {
5909 controller.push_basic_input(FAST_THRUST);
5910 }
5911
5912 agent.combat_state.conditions
5913 [ActionStateConditions::VerticalStrikeCombo as usize] = false;
5914 } else if agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize]
5915 {
5916 controller.push_basic_input(WHIRLWIND);
5917 agent.combat_state.conditions[ActionStateConditions::WhirlwindTwice as usize] =
5918 false;
5919 } else if agent.combat_state.timers[ActionStateTimers::OutOfMeleeRange as usize]
5920 > FORCE_GAP_CLOSER_TIMEOUT
5921 {
5922 controller.push_basic_input(LAVA_LEAP);
5924 } else if attack_data.dist_sqrd < MELEE_RANGE.powi(2) {
5925 if tgt_missed_parry {
5926 controller.push_basic_input(PARRY_PUNISH);
5927 agent.combat_state.conditions
5928 [ActionStateConditions::VerticalStrikeCombo as usize] = true;
5929 } else if agent.combat_state.timers[ActionStateTimers::Special as usize] > 10.0 {
5930 let rand_special = rand_special(rng);
5932 match rand_special {
5933 VERTICAL_STRIKE => {
5934 agent.combat_state.conditions
5935 [ActionStateConditions::VerticalStrikeCombo as usize] = true
5936 },
5937 WHIRLWIND if rng.random_bool(0.2) => {
5938 agent.combat_state.conditions
5939 [ActionStateConditions::WhirlwindTwice as usize] = true
5940 },
5941 _ => {},
5942 }
5943 controller.push_basic_input(rand_special);
5944
5945 agent.combat_state.timers[ActionStateTimers::Special as usize] =
5946 rng.random_range(0.0..3.0 + 5.0 * damage_fraction);
5947 } else if attack_data.angle > 90.0 {
5948 let rand_aoe = rand_aoe(rng);
5950 match rand_aoe {
5951 WHIRLWIND if rng.random_bool(0.2) => {
5952 agent.combat_state.conditions
5953 [ActionStateConditions::WhirlwindTwice as usize] = true
5954 },
5955 _ => {},
5956 }
5957
5958 controller.push_basic_input(rand_aoe);
5959 } else {
5960 controller.push_basic_input(rand_basic(rng, damage_fraction));
5962 }
5963 } else if attack_data.dist_sqrd < RANGED_RANGE.powi(2) {
5964 if rng.random_bool(0.05) {
5966 controller.push_basic_input(rand_ranged(rng));
5967 }
5968 } else if attack_data.dist_sqrd < LEAP_RANGE.powi(2) {
5969 controller.push_basic_input(LAVA_LEAP);
5971 } else if rng.random_bool(0.1) {
5972 cast_targeted_fire_pillar(controller);
5974 }
5975 }
5976
5977 self.path_toward_target(
5978 agent,
5979 controller,
5980 tgt_data.pos.0,
5981 read_data,
5982 Path::AtTarget,
5983 attack_data.in_min_range().then_some(0.1),
5984 );
5985
5986 if self.physics_state.in_liquid().is_some() {
5988 controller.push_basic_input(InputKind::Jump);
5989 }
5990 if self.physics_state.in_liquid().is_some() {
5991 controller.inputs.move_z = 1.0;
5992 }
5993 }
5994
5995 pub fn handle_ashen_axe_attack(
5996 &self,
5997 agent: &mut Agent,
5998 controller: &mut Controller,
5999 attack_data: &AttackData,
6000 tgt_data: &TargetData,
6001 read_data: &ReadData,
6002 rng: &mut impl Rng,
6003 ) {
6004 const IMMOLATION_COOLDOWN: f32 = 50.0;
6005 const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6006 desired_energy: 30.0,
6007 combo_scaling_buildup: 0,
6008 };
6009
6010 enum ActionStateTimers {
6011 SinceSelfImmolation,
6012 }
6013
6014 const DOUBLE_STRIKE: InputKind = InputKind::Primary;
6015 const FLAME_WAVE: InputKind = InputKind::Secondary;
6016 const KNOCKBACK_COMBO: InputKind = InputKind::Ability(0);
6017 const SELF_IMMOLATION: InputKind = InputKind::Ability(1);
6018
6019 fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6020 !matches!(
6021 char_state,
6022 CharacterState::ComboMelee2(_)
6023 | CharacterState::Shockwave(_)
6024 | CharacterState::SelfBuff(_)
6025 )
6026 }
6027
6028 let could_use = |input| {
6029 Option::<AbilityInput>::from(input)
6030 .and_then(|ability_input| self.extract_ability(ability_input))
6031 .is_some_and(|ability_data| {
6032 ability_data.could_use(
6033 attack_data,
6034 self,
6035 tgt_data,
6036 read_data,
6037 ABILITY_PREFERENCES,
6038 )
6039 })
6040 };
6041
6042 if !agent.combat_state.initialized {
6044 agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6045 IMMOLATION_COOLDOWN;
6046 agent.combat_state.initialized = true;
6047 }
6048
6049 agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] +=
6050 read_data.dt.0;
6051
6052 if self
6053 .char_state
6054 .ability_info()
6055 .map(|ai| ai.input)
6056 .is_some_and(|input_kind| input_kind == KNOCKBACK_COMBO)
6057 {
6058 controller.push_basic_input(KNOCKBACK_COMBO);
6059 } else if can_cast_new_ability(self.char_state)
6060 && agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize]
6061 >= IMMOLATION_COOLDOWN
6062 && could_use(SELF_IMMOLATION)
6063 {
6064 agent.combat_state.timers[ActionStateTimers::SinceSelfImmolation as usize] =
6065 rng.random_range(0.0..5.0);
6066
6067 controller.push_basic_input(SELF_IMMOLATION);
6068 } else if rng.random_bool(0.35) && could_use(KNOCKBACK_COMBO) {
6069 controller.push_basic_input(KNOCKBACK_COMBO);
6070 } else if could_use(DOUBLE_STRIKE) {
6071 controller.push_basic_input(DOUBLE_STRIKE);
6072 } else if rng.random_bool(0.2) && could_use(FLAME_WAVE) {
6073 controller.push_basic_input(FLAME_WAVE);
6074 }
6075
6076 self.path_toward_target(
6077 agent,
6078 controller,
6079 tgt_data.pos.0,
6080 read_data,
6081 Path::AtTarget,
6082 None,
6083 );
6084 }
6085
6086 pub fn handle_ashen_staff_attack(
6087 &self,
6088 agent: &mut Agent,
6089 controller: &mut Controller,
6090 attack_data: &AttackData,
6091 tgt_data: &TargetData,
6092 read_data: &ReadData,
6093 rng: &mut impl Rng,
6094 ) {
6095 const ABILITY_COOLDOWN: f32 = 50.0;
6096 const INITIAL_COOLDOWN: f32 = ABILITY_COOLDOWN - 10.0;
6097 const ABILITY_PREFERENCES: AbilityPreferences = AbilityPreferences {
6098 desired_energy: 40.0,
6099 combo_scaling_buildup: 0,
6100 };
6101
6102 enum ActionStateTimers {
6103 SinceAbility,
6104 }
6105
6106 const FIREBALL: InputKind = InputKind::Primary;
6107 const FLAME_WALL: InputKind = InputKind::Ability(0);
6108 const SUMMON_CRUX: InputKind = InputKind::Ability(1);
6109
6110 fn can_cast_new_ability(char_state: &CharacterState) -> bool {
6111 !matches!(
6112 char_state,
6113 CharacterState::BasicRanged(_)
6114 | CharacterState::BasicBeam(_)
6115 | CharacterState::RapidMelee(_)
6116 | CharacterState::BasicAura(_)
6117 )
6118 }
6119
6120 let could_use = |input| {
6121 Option::<AbilityInput>::from(input)
6122 .and_then(|ability_input| self.extract_ability(ability_input))
6123 .is_some_and(|ability_data| {
6124 ability_data.could_use(
6125 attack_data,
6126 self,
6127 tgt_data,
6128 read_data,
6129 ABILITY_PREFERENCES,
6130 )
6131 })
6132 };
6133
6134 if !agent.combat_state.initialized {
6136 agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] = INITIAL_COOLDOWN;
6137 agent.combat_state.initialized = true;
6138 }
6139
6140 agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] += read_data.dt.0;
6141
6142 if can_cast_new_ability(self.char_state)
6143 && agent.combat_state.timers[ActionStateTimers::SinceAbility as usize]
6144 >= ABILITY_COOLDOWN
6145 && (could_use(FLAME_WALL) || could_use(SUMMON_CRUX))
6146 {
6147 agent.combat_state.timers[ActionStateTimers::SinceAbility as usize] =
6148 rng.random_range(0.0..5.0);
6149
6150 if could_use(FLAME_WALL) && (rng.random_bool(0.5) || !could_use(SUMMON_CRUX)) {
6151 controller.push_basic_input(FLAME_WALL);
6152 } else {
6153 controller.push_basic_input(SUMMON_CRUX);
6154 }
6155 } else if rng.random_bool(0.5) && could_use(FIREBALL) {
6156 controller.push_basic_input(FIREBALL);
6157 }
6158
6159 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6160 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6162 &*read_data.terrain,
6163 self.pos.0,
6164 self.vel.0,
6165 tgt_data.pos.0,
6166 TraversalConfig {
6167 min_tgt_dist: 1.25,
6168 ..self.traversal_config
6169 },
6170 &read_data.time,
6171 ) {
6172 self.unstuck_if(stuck, controller);
6173 controller.inputs.move_dir =
6174 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6175 }
6176 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6177 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6179 &*read_data.terrain,
6180 self.pos.0,
6181 self.vel.0,
6182 tgt_data.pos.0,
6183 TraversalConfig {
6184 min_tgt_dist: 1.25,
6185 ..self.traversal_config
6186 },
6187 &read_data.time,
6188 ) {
6189 self.unstuck_if(stuck, controller);
6190 if entities_have_line_of_sight(
6191 self.pos,
6192 self.body,
6193 self.scale,
6194 tgt_data.pos,
6195 tgt_data.body,
6196 tgt_data.scale,
6197 read_data,
6198 ) && attack_data.angle < 45.0
6199 {
6200 controller.inputs.move_dir = bearing
6201 .xy()
6202 .rotated_z(rng.random_range(-1.57..-0.5))
6203 .try_normalized()
6204 .unwrap_or_else(Vec2::zero)
6205 * speed;
6206 } else {
6207 controller.inputs.move_dir =
6209 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6210 self.jump_if(bearing.z > 1.5, controller);
6211 controller.inputs.move_z = bearing.z;
6212 }
6213 }
6214 } else {
6215 self.path_toward_target(
6217 agent,
6218 controller,
6219 tgt_data.pos.0,
6220 read_data,
6221 Path::AtTarget,
6222 None,
6223 );
6224 }
6225 }
6226
6227 pub fn handle_cardinal_attack(
6228 &self,
6229 agent: &mut Agent,
6230 controller: &mut Controller,
6231 attack_data: &AttackData,
6232 tgt_data: &TargetData,
6233 read_data: &ReadData,
6234 rng: &mut impl Rng,
6235 ) {
6236 const DESIRED_ENERGY_LEVEL: f32 = 50.0;
6237 const DESIRED_COMBO_LEVEL: u32 = 8;
6238 const MINION_SUMMON_THRESHOLD: f32 = 0.10;
6239
6240 enum ActionStateConditions {
6241 ConditionCounterInitialized = 0,
6242 }
6243
6244 enum ActionStateFCounters {
6245 FCounterHealthThreshold = 0,
6246 }
6247
6248 let health_fraction = self.health.map_or(0.5, |h| h.fraction());
6249 if !agent.combat_state.conditions
6252 [ActionStateConditions::ConditionCounterInitialized as usize]
6253 {
6254 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6255 1.0 - MINION_SUMMON_THRESHOLD;
6256 agent.combat_state.conditions
6257 [ActionStateConditions::ConditionCounterInitialized as usize] = true;
6258 }
6259
6260 if agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize]
6261 > health_fraction
6262 {
6263 controller.push_basic_input(InputKind::Ability(1));
6265
6266 if matches!(self.char_state, CharacterState::BasicSummon(c) if matches!(c.stage_section, StageSection::Recover))
6267 {
6268 agent.combat_state.counters
6269 [ActionStateFCounters::FCounterHealthThreshold as usize] -=
6270 MINION_SUMMON_THRESHOLD;
6271 }
6272 }
6273 else if attack_data.dist_sqrd > attack_data.min_attack_dist.powi(2)
6275 && entities_have_line_of_sight(
6276 self.pos,
6277 self.body,
6278 self.scale,
6279 tgt_data.pos,
6280 tgt_data.body,
6281 tgt_data.scale,
6282 read_data,
6283 )
6284 {
6285 if self.energy.current() > DESIRED_ENERGY_LEVEL
6288 && read_data
6289 .combos
6290 .get(*self.entity)
6291 .is_some_and(|c| c.counter() >= DESIRED_COMBO_LEVEL)
6292 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6293 buff.iter_kind(BuffKind::Regeneration)
6294 .peekable()
6295 .peek()
6296 .is_some()
6297 })
6298 {
6299 controller.push_basic_input(InputKind::Secondary);
6301 } else if self
6302 .skill_set
6303 .has_skill(Skill::Sceptre(SceptreSkill::UnlockAura))
6304 && self.energy.current() > DESIRED_ENERGY_LEVEL
6305 && !read_data.buffs.get(*self.entity).iter().any(|buff| {
6306 buff.iter_kind(BuffKind::ProtectingWard)
6307 .peekable()
6308 .peek()
6309 .is_some()
6310 })
6311 {
6312 controller.push_basic_input(InputKind::Ability(0));
6315 } else {
6316 controller.push_basic_input(InputKind::Primary);
6319 }
6320 } else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6321 if self.body.is_some_and(|b| b.is_humanoid())
6322 && self.energy.current()
6323 > CharacterAbility::default_roll(Some(self.char_state)).energy_cost()
6324 && !matches!(self.char_state, CharacterState::BasicAura(c) if !matches!(c.stage_section, StageSection::Recover))
6325 {
6326 controller.push_basic_input(InputKind::Ability(0));
6328 } else if attack_data.angle < 15.0 {
6329 controller.push_basic_input(InputKind::Primary);
6330 }
6331 }
6332 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6335 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6337 &*read_data.terrain,
6338 self.pos.0,
6339 self.vel.0,
6340 tgt_data.pos.0,
6341 TraversalConfig {
6342 min_tgt_dist: 1.25,
6343 ..self.traversal_config
6344 },
6345 &read_data.time,
6346 ) {
6347 self.unstuck_if(stuck, controller);
6348 controller.inputs.move_dir =
6349 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6350 }
6351 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6352 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6354 &*read_data.terrain,
6355 self.pos.0,
6356 self.vel.0,
6357 tgt_data.pos.0,
6358 TraversalConfig {
6359 min_tgt_dist: 1.25,
6360 ..self.traversal_config
6361 },
6362 &read_data.time,
6363 ) {
6364 self.unstuck_if(stuck, controller);
6365 if entities_have_line_of_sight(
6366 self.pos,
6367 self.body,
6368 self.scale,
6369 tgt_data.pos,
6370 tgt_data.body,
6371 tgt_data.scale,
6372 read_data,
6373 ) && attack_data.angle < 45.0
6374 {
6375 controller.inputs.move_dir = bearing
6376 .xy()
6377 .rotated_z(rng.random_range(0.5..1.57))
6378 .try_normalized()
6379 .unwrap_or_else(Vec2::zero)
6380 * speed;
6381 } else {
6382 controller.inputs.move_dir =
6384 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6385 self.jump_if(bearing.z > 1.5, controller);
6386 controller.inputs.move_z = bearing.z;
6387 }
6388 }
6389 if self.body.map(|b| b.is_humanoid()).unwrap_or(false)
6391 && !matches!(self.char_state, CharacterState::BasicAura(_))
6392 && attack_data.dist_sqrd < 16.0f32.powi(2)
6393 && rng.random::<f32>() < 0.01
6394 {
6395 controller.push_basic_input(InputKind::Roll);
6396 }
6397 } else {
6398 self.path_toward_target(
6400 agent,
6401 controller,
6402 tgt_data.pos.0,
6403 read_data,
6404 Path::AtTarget,
6405 None,
6406 );
6407 }
6408 }
6409
6410 pub fn handle_sea_bishop_attack(
6411 &self,
6412 agent: &mut Agent,
6413 controller: &mut Controller,
6414 attack_data: &AttackData,
6415 tgt_data: &TargetData,
6416 read_data: &ReadData,
6417 rng: &mut impl Rng,
6418 ) {
6419 let line_of_sight_with_target = || {
6420 entities_have_line_of_sight(
6421 self.pos,
6422 self.body,
6423 self.scale,
6424 tgt_data.pos,
6425 tgt_data.body,
6426 tgt_data.scale,
6427 read_data,
6428 )
6429 };
6430
6431 enum ActionStateTimers {
6432 TimerBeam = 0,
6433 }
6434 if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 6.0 {
6435 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
6436 } else {
6437 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
6438 }
6439
6440 if line_of_sight_with_target()
6442 && agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 3.0
6443 {
6444 controller.push_basic_input(InputKind::Primary);
6445 }
6446 if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6449 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6451 &*read_data.terrain,
6452 self.pos.0,
6453 self.vel.0,
6454 tgt_data.pos.0,
6455 TraversalConfig {
6456 min_tgt_dist: 1.25,
6457 ..self.traversal_config
6458 },
6459 &read_data.time,
6460 ) {
6461 self.unstuck_if(stuck, controller);
6462 controller.inputs.move_dir =
6463 -bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6464 }
6465 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6466 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
6468 &*read_data.terrain,
6469 self.pos.0,
6470 self.vel.0,
6471 tgt_data.pos.0,
6472 TraversalConfig {
6473 min_tgt_dist: 1.25,
6474 ..self.traversal_config
6475 },
6476 &read_data.time,
6477 ) {
6478 self.unstuck_if(stuck, controller);
6479 if line_of_sight_with_target() && attack_data.angle < 45.0 {
6480 controller.inputs.move_dir = bearing
6481 .xy()
6482 .rotated_z(rng.random_range(0.5..1.57))
6483 .try_normalized()
6484 .unwrap_or_else(Vec2::zero)
6485 * speed;
6486 } else {
6487 controller.inputs.move_dir =
6489 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
6490 self.jump_if(bearing.z > 1.5, controller);
6491 controller.inputs.move_z = bearing.z;
6492 }
6493 }
6494 } else {
6495 self.path_toward_target(
6497 agent,
6498 controller,
6499 tgt_data.pos.0,
6500 read_data,
6501 Path::AtTarget,
6502 None,
6503 );
6504 }
6505 }
6506
6507 pub fn handle_cursekeeper_attack(
6508 &self,
6509 agent: &mut Agent,
6510 controller: &mut Controller,
6511 attack_data: &AttackData,
6512 tgt_data: &TargetData,
6513 read_data: &ReadData,
6514 rng: &mut impl Rng,
6515 ) {
6516 enum ActionStateTimers {
6517 TimerBeam,
6518 TimerSummon,
6519 SelectSummon,
6520 }
6521 if tgt_data.pos.0.z - self.pos.0.z > 3.5 {
6522 controller.push_action(ControlAction::StartInput {
6523 input: InputKind::Ability(4),
6524 target_entity: agent
6525 .target
6526 .as_ref()
6527 .and_then(|t| read_data.uids.get(t.target))
6528 .copied(),
6529 select_pos: None,
6530 });
6531 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 12.0 {
6532 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] = 0.0;
6533 } else {
6534 agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] += read_data.dt.0;
6535 }
6536
6537 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
6538 {
6539 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] = 0.0;
6540 agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] =
6541 rng.random_range(0..=3) as f32;
6542 } else {
6543 agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] += read_data.dt.0;
6544 }
6545
6546 if agent.combat_state.timers[ActionStateTimers::TimerSummon as usize] > 32.0 {
6547 match agent.combat_state.timers[ActionStateTimers::SelectSummon as usize] as i32 {
6548 0 => controller.push_basic_input(InputKind::Ability(0)),
6549 1 => controller.push_basic_input(InputKind::Ability(1)),
6550 2 => controller.push_basic_input(InputKind::Ability(2)),
6551 _ => controller.push_basic_input(InputKind::Ability(3)),
6552 }
6553 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 6.0 {
6554 controller.push_basic_input(InputKind::Ability(5));
6555 } else if agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] < 9.0 {
6556 controller.push_basic_input(InputKind::Primary);
6557 } else {
6558 controller.push_basic_input(InputKind::Secondary);
6559 }
6560
6561 if attack_data.dist_sqrd > 10_f32.powi(2)
6562 || agent.combat_state.timers[ActionStateTimers::TimerBeam as usize] > 4.0
6563 {
6564 self.path_toward_target(
6565 agent,
6566 controller,
6567 tgt_data.pos.0,
6568 read_data,
6569 Path::AtTarget,
6570 None,
6571 );
6572 }
6573 }
6574
6575 pub fn handle_shamanic_spirit_attack(
6576 &self,
6577 agent: &mut Agent,
6578 controller: &mut Controller,
6579 attack_data: &AttackData,
6580 tgt_data: &TargetData,
6581 read_data: &ReadData,
6582 ) {
6583 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
6584 controller.push_action(ControlAction::StartInput {
6585 input: InputKind::Secondary,
6586 target_entity: agent
6587 .target
6588 .as_ref()
6589 .and_then(|t| read_data.uids.get(t.target))
6590 .copied(),
6591 select_pos: None,
6592 });
6593 } else if attack_data.in_min_range() && attack_data.angle < 30.0 {
6594 controller.push_basic_input(InputKind::Primary);
6595 controller.inputs.move_dir = Vec2::zero();
6596 } else {
6597 self.path_toward_target(
6598 agent,
6599 controller,
6600 tgt_data.pos.0,
6601 read_data,
6602 Path::AtTarget,
6603 None,
6604 );
6605 }
6606 }
6607
6608 pub fn handle_cursekeeper_fake_attack(
6609 &self,
6610 controller: &mut Controller,
6611 attack_data: &AttackData,
6612 ) {
6613 if attack_data.dist_sqrd < 25_f32.powi(2) {
6614 controller.push_basic_input(InputKind::Primary);
6615 }
6616 }
6617
6618 pub fn handle_karkatha_attack(
6619 &self,
6620 agent: &mut Agent,
6621 controller: &mut Controller,
6622 attack_data: &AttackData,
6623 tgt_data: &TargetData,
6624 read_data: &ReadData,
6625 _rng: &mut impl Rng,
6626 ) {
6627 enum ActionStateTimers {
6628 RiposteTimer,
6629 SummonTimer,
6630 }
6631
6632 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] += read_data.dt.0;
6633 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] += read_data.dt.0;
6634 if matches!(self.char_state, CharacterState::RiposteMelee(c) if !matches!(c.stage_section, StageSection::Recover))
6635 {
6636 agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] = 0.0;
6638 }
6639 if matches!(self.char_state, CharacterState::BasicSummon(c) if !matches!(c.stage_section, StageSection::Recover))
6640 {
6641 agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] = 0.0;
6643 }
6644 let home = agent.patrol_origin.unwrap_or(self.pos.0);
6646 let dest = if tgt_data.pos.0.z < self.pos.0.z {
6647 home
6648 } else {
6649 tgt_data.pos.0
6650 };
6651 if attack_data.in_min_range() {
6652 if agent.combat_state.timers[ActionStateTimers::RiposteTimer as usize] > 3.0 {
6653 controller.push_basic_input(InputKind::Ability(2));
6654 } else {
6655 controller.push_basic_input(InputKind::Primary);
6656 };
6657 } else if attack_data.dist_sqrd < 20.0_f32.powi(2) {
6658 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] > 20.0 {
6659 controller.push_basic_input(InputKind::Ability(1));
6660 } else {
6661 controller.push_basic_input(InputKind::Secondary);
6662 }
6663 } else if attack_data.dist_sqrd < 30.0_f32.powi(2) {
6664 if agent.combat_state.timers[ActionStateTimers::SummonTimer as usize] < 10.0 {
6665 self.path_toward_target(
6666 agent,
6667 controller,
6668 tgt_data.pos.0,
6669 read_data,
6670 Path::AtTarget,
6671 None,
6672 );
6673 } else {
6674 controller.push_basic_input(InputKind::Ability(0));
6675 }
6676 } else {
6677 self.path_toward_target(agent, controller, dest, read_data, Path::AtTarget, None);
6678 }
6679 }
6680
6681 pub fn handle_dagon_attack(
6682 &self,
6683 agent: &mut Agent,
6684 controller: &mut Controller,
6685 attack_data: &AttackData,
6686 tgt_data: &TargetData,
6687 read_data: &ReadData,
6688 ) {
6689 enum ActionStateTimers {
6690 TimerDagon = 0,
6691 }
6692 let line_of_sight_with_target = || {
6693 entities_have_line_of_sight(
6694 self.pos,
6695 self.body,
6696 self.scale,
6697 tgt_data.pos,
6698 tgt_data.body,
6699 tgt_data.scale,
6700 read_data,
6701 )
6702 };
6703 let home = agent.patrol_origin.unwrap_or(self.pos.0);
6705 let exit = Vec3::new(home.x - 6.0, home.y - 6.0, home.z);
6706 let (station_0, station_1) = (exit + 12.0, exit - 12.0);
6707 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.5 {
6708 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] = 0.0;
6709 }
6710 if !line_of_sight_with_target()
6711 && (tgt_data.pos.0 - exit).xy().magnitude_squared() < (10.0_f32).powi(2)
6712 {
6713 let station = if (tgt_data.pos.0 - station_0).xy().magnitude_squared()
6714 < (tgt_data.pos.0 - station_1).xy().magnitude_squared()
6715 {
6716 station_0
6717 } else {
6718 station_1
6719 };
6720 self.path_toward_target(agent, controller, station, read_data, Path::AtTarget, None);
6721 }
6722 else if attack_data.dist_sqrd < (2.0 * attack_data.min_attack_dist).powi(2) {
6724 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6725 controller.push_basic_input(InputKind::Primary);
6726 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6727 } else {
6728 controller.push_basic_input(InputKind::Secondary);
6729 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6730 }
6731 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6733 controller.inputs.move_dir = Vec2::zero();
6734 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6735 controller.push_basic_input(InputKind::Primary);
6736 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6737 } else {
6738 controller.push_basic_input(InputKind::Ability(1));
6739 }
6740 } else if attack_data.dist_sqrd > (4.0 * attack_data.min_attack_dist).powi(2) {
6741 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 2.0 {
6743 controller.push_basic_input(InputKind::Primary);
6744 } else {
6745 controller.push_basic_input(InputKind::Ability(2));
6746 }
6747 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6748 } else if line_of_sight_with_target() {
6749 if agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] > 1.0 {
6751 controller.push_basic_input(InputKind::Primary);
6752 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6753 } else {
6754 controller.push_basic_input(InputKind::Ability(0));
6755 agent.combat_state.timers[ActionStateTimers::TimerDagon as usize] += read_data.dt.0;
6756 }
6757 }
6758 let path = if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
6760 Path::Separate
6761 } else {
6762 Path::AtTarget
6763 };
6764 self.path_toward_target(agent, controller, tgt_data.pos.0, read_data, path, None);
6765 }
6766
6767 pub fn handle_snaretongue_attack(
6768 &self,
6769 agent: &mut Agent,
6770 controller: &mut Controller,
6771 attack_data: &AttackData,
6772 read_data: &ReadData,
6773 ) {
6774 enum Timers {
6775 TimerAttack = 0,
6776 }
6777 let attack_timer = &mut agent.combat_state.timers[Timers::TimerAttack as usize];
6778 if *attack_timer > 2.5 {
6779 *attack_timer = 0.0;
6780 }
6781 if attack_data.dist_sqrd < attack_data.min_attack_dist.powi(2) {
6783 if *attack_timer > 0.5 {
6784 controller.push_basic_input(InputKind::Primary);
6785 *attack_timer += read_data.dt.0;
6786 } else {
6787 controller.push_basic_input(InputKind::Secondary);
6788 *attack_timer += read_data.dt.0;
6789 }
6790 } else if attack_data.dist_sqrd < (3.0 * attack_data.min_attack_dist).powi(2) {
6792 controller.inputs.move_dir = Vec2::zero();
6793 if *attack_timer > 2.0 {
6794 controller.push_basic_input(InputKind::Ability(0));
6795 *attack_timer += read_data.dt.0;
6796 } else {
6797 controller.push_basic_input(InputKind::Ability(1));
6798 }
6799 } else {
6800 if *attack_timer > 1.0 {
6802 controller.push_basic_input(InputKind::Ability(0));
6803 *attack_timer += read_data.dt.0;
6804 } else {
6805 controller.push_basic_input(InputKind::Ability(2));
6806 *attack_timer += read_data.dt.0;
6807 }
6808 }
6809 }
6810
6811 pub fn handle_deadwood(
6812 &self,
6813 agent: &mut Agent,
6814 controller: &mut Controller,
6815 attack_data: &AttackData,
6816 tgt_data: &TargetData,
6817 read_data: &ReadData,
6818 ) {
6819 const BEAM_RANGE: f32 = 20.0;
6820 const BEAM_TIME: Duration = Duration::from_secs(3);
6821 if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
6823 {
6824 controller.push_basic_input(InputKind::Secondary);
6826 controller.inputs.move_dir = self.ori.look_vec().xy();
6827 } else if attack_data.in_min_range() && attack_data.angle_xy < 10.0 {
6828 controller.push_basic_input(InputKind::Secondary);
6830 } else if matches!(self.char_state, CharacterState::BasicBeam(s) if s.stage_section != StageSection::Recover && s.timer < BEAM_TIME)
6831 {
6832 controller.push_basic_input(InputKind::Primary);
6834 } else if attack_data.dist_sqrd < BEAM_RANGE.powi(2) {
6835 if attack_data.angle_xy < 5.0 {
6837 controller.push_basic_input(InputKind::Primary);
6838 } else {
6839 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
6841 .xy()
6842 .try_normalized()
6843 .unwrap_or_else(Vec2::zero)
6844 * 0.01;
6845 }
6846 } else {
6847 self.path_toward_target(
6849 agent,
6850 controller,
6851 tgt_data.pos.0,
6852 read_data,
6853 Path::AtTarget,
6854 None,
6855 );
6856 }
6857 }
6858
6859 pub fn handle_mandragora(
6860 &self,
6861 agent: &mut Agent,
6862 controller: &mut Controller,
6863 attack_data: &AttackData,
6864 tgt_data: &TargetData,
6865 read_data: &ReadData,
6866 ) {
6867 const SCREAM_RANGE: f32 = 10.0; enum ActionStateFCounters {
6870 FCounterHealthThreshold = 0,
6871 }
6872
6873 enum ActionStateConditions {
6874 ConditionHasScreamed = 0,
6875 }
6876
6877 if !agent.combat_state.initialized {
6878 agent.combat_state.counters[ActionStateFCounters::FCounterHealthThreshold as usize] =
6879 self.health.map_or(0.0, |h| h.maximum());
6880 agent.combat_state.initialized = true;
6881 }
6882
6883 if !agent.combat_state.conditions[ActionStateConditions::ConditionHasScreamed as usize] {
6884 if self.health.is_some_and(|h| {
6887 h.current()
6888 < agent.combat_state.counters
6889 [ActionStateFCounters::FCounterHealthThreshold as usize]
6890 }) || attack_data.dist_sqrd < SCREAM_RANGE.powi(2)
6891 {
6892 agent.combat_state.conditions
6893 [ActionStateConditions::ConditionHasScreamed as usize] = true;
6894 controller.push_basic_input(InputKind::Secondary);
6895 }
6896 } else {
6897 if attack_data.in_min_range() {
6899 controller.push_basic_input(InputKind::Primary);
6900 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
6901 && entities_have_line_of_sight(
6902 self.pos,
6903 self.body,
6904 self.scale,
6905 tgt_data.pos,
6906 tgt_data.body,
6907 tgt_data.scale,
6908 read_data,
6909 )
6910 {
6911 self.path_toward_target(
6913 agent,
6914 controller,
6915 tgt_data.pos.0,
6916 read_data,
6917 Path::AtTarget,
6918 None,
6919 );
6920 } else {
6921 agent.combat_state.conditions
6923 [ActionStateConditions::ConditionHasScreamed as usize] = false;
6924 agent.combat_state.counters
6925 [ActionStateFCounters::FCounterHealthThreshold as usize] =
6926 self.health.map_or(0.0, |h| h.maximum());
6927 }
6928 }
6929 }
6930
6931 pub fn handle_wood_golem(
6932 &self,
6933 agent: &mut Agent,
6934 controller: &mut Controller,
6935 attack_data: &AttackData,
6936 tgt_data: &TargetData,
6937 read_data: &ReadData,
6938 rng: &mut impl Rng,
6939 ) {
6940 const PATH_RANGE_FACTOR: f32 = 0.3; const STRIKE_RANGE_FACTOR: f32 = 0.6; const STRIKE_AIM_FACTOR: f32 = 0.7;
6955 const SPIN_RANGE_FACTOR: f32 = 0.6;
6956 const SPIN_COOLDOWN: f32 = 1.5;
6957 const SPIN_RELAX_FACTOR: f32 = 0.2;
6958 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.7;
6959 const SHOCKWAVE_AIM_FACTOR: f32 = 0.4;
6960 const SHOCKWAVE_COOLDOWN: f32 = 5.0;
6961 const MIXUP_COOLDOWN: f32 = 2.5;
6962 const MIXUP_RELAX_FACTOR: f32 = 0.3;
6963
6964 const SPIN: usize = 0;
6966 const SHOCKWAVE: usize = 1;
6967 const MIXUP: usize = 2;
6968
6969 let shockwave_min_range = self.body.map_or(0.0, |b| b.height() * 1.1);
6972
6973 let (strike_range, strike_angle) = {
6975 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
6976 self.extract_ability(AbilityInput::Primary)
6977 {
6978 (range, angle)
6979 } else {
6980 (0.0, 0.0)
6981 }
6982 };
6983 let spin_range = {
6984 if let Some(AbilityData::BasicMelee { range, .. }) =
6985 self.extract_ability(AbilityInput::Secondary)
6986 {
6987 range
6988 } else {
6989 0.0
6990 }
6991 };
6992 let (shockwave_max_range, shockwave_angle) = {
6993 if let Some(AbilityData::Shockwave { range, angle, .. }) =
6994 self.extract_ability(AbilityInput::Auxiliary(0))
6995 {
6996 (range, angle)
6997 } else {
6998 (0.0, 0.0)
6999 }
7000 };
7001
7002 let is_in_spin_range = attack_data.dist_sqrd
7004 < (attack_data.body_dist + spin_range * SPIN_RANGE_FACTOR).powi(2);
7005 let is_in_strike_range = attack_data.dist_sqrd
7006 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7007 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7008
7009 let current_input = self.char_state.ability_info().map(|ai| ai.input);
7014 if matches!(current_input, Some(InputKind::Secondary)) {
7015 agent.combat_state.timers[SPIN] = 0.0;
7017 agent.combat_state.timers[MIXUP] = 0.0;
7018 } else if is_in_spin_range && !(is_in_strike_range && is_in_strike_angle) {
7019 agent.combat_state.timers[SPIN] += read_data.dt.0;
7021 } else {
7022 agent.combat_state.timers[SPIN] =
7024 (agent.combat_state.timers[SPIN] - read_data.dt.0 * SPIN_RELAX_FACTOR).max(0.0);
7025 }
7026 if matches!(self.char_state, CharacterState::Shockwave(_)) {
7028 agent.combat_state.timers[SHOCKWAVE] = 0.0;
7030 agent.combat_state.timers[MIXUP] = 0.0;
7031 } else {
7032 agent.combat_state.timers[SHOCKWAVE] += read_data.dt.0;
7034 }
7035 if is_in_strike_range && is_in_strike_angle {
7037 agent.combat_state.timers[MIXUP] += read_data.dt.0;
7039 } else {
7040 agent.combat_state.timers[MIXUP] =
7042 (agent.combat_state.timers[MIXUP] - read_data.dt.0 * MIXUP_RELAX_FACTOR).max(0.0);
7043 }
7044
7045 if is_in_strike_range && is_in_strike_angle {
7048 if agent.combat_state.timers[MIXUP] > MIXUP_COOLDOWN {
7050 let randomise: u8 = rng.random_range(1..=3);
7051 match randomise {
7052 1 => controller.push_basic_input(InputKind::Ability(0)), 2 => controller.push_basic_input(InputKind::Primary), _ => controller.push_basic_input(InputKind::Secondary), }
7056 }
7057 else {
7059 controller.push_basic_input(InputKind::Primary);
7060 }
7061 }
7062 else if is_in_spin_range || (is_in_strike_range && !is_in_strike_angle) {
7064 if agent.combat_state.timers[SPIN] > SPIN_COOLDOWN {
7066 controller.push_basic_input(InputKind::Secondary);
7067 }
7068 }
7070 else if attack_data.dist_sqrd > shockwave_min_range.powi(2)
7072 && attack_data.dist_sqrd < (shockwave_max_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7073 && attack_data.angle < shockwave_angle * SHOCKWAVE_AIM_FACTOR
7074 {
7075 if agent.combat_state.timers[SHOCKWAVE] > SHOCKWAVE_COOLDOWN {
7077 controller.push_basic_input(InputKind::Ability(0));
7078 }
7079 }
7081
7082 if attack_data.dist_sqrd
7085 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7086 {
7087 self.path_toward_target(
7088 agent,
7089 controller,
7090 tgt_data.pos.0,
7091 read_data,
7092 Path::AtTarget,
7093 None,
7094 );
7095 }
7096 else if attack_data.angle > 0.0 {
7098 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7100 .xy()
7101 .try_normalized()
7102 .unwrap_or_else(Vec2::zero)
7103 * 0.001; }
7105 }
7106
7107 pub fn handle_gnarling_chieftain(
7108 &self,
7109 agent: &mut Agent,
7110 controller: &mut Controller,
7111 attack_data: &AttackData,
7112 tgt_data: &TargetData,
7113 read_data: &ReadData,
7114 rng: &mut impl Rng,
7115 ) {
7116 const PATH_RANGE_FACTOR: f32 = 0.4;
7131 const STRIKE_RANGE_FACTOR: f32 = 0.7;
7132 const STRIKE_AIM_FACTOR: f32 = 0.8;
7133 const BARRAGE_RANGE_FACTOR: f32 = 0.8;
7134 const BARRAGE_AIM_FACTOR: f32 = 0.65;
7135 const SHOCKWAVE_RANGE_FACTOR: f32 = 0.75;
7136 const TOTEM_COOLDOWN: f32 = 25.0;
7137 const HEAVY_ATTACK_COOLDOWN_SPAN: [f32; 2] = [8.0, 13.0];
7138 const HEAVY_ATTACK_CHARGE_FACTOR: f32 = 3.3;
7139 const HEAVY_ATTACK_FAST_CHARGE_FACTOR: f32 = 5.0;
7140
7141 const HAS_SUMMONED_FIRST_TOTEM: usize = 0;
7143 const SUMMON_TOTEM: usize = 0;
7145 const HEAVY_ATTACK: usize = 1;
7146 const HEAVY_ATTACK_COOLDOWN: usize = 0;
7148
7149 let line_of_sight_with_target = || {
7151 entities_have_line_of_sight(
7152 self.pos,
7153 self.body,
7154 self.scale,
7155 tgt_data.pos,
7156 tgt_data.body,
7157 tgt_data.scale,
7158 read_data,
7159 )
7160 };
7161
7162 let (strike_range, strike_angle) = {
7165 if let Some(AbilityData::BasicMelee { range, angle, .. }) =
7166 self.extract_ability(AbilityInput::Primary)
7167 {
7168 (range, angle)
7169 } else {
7170 (0.0, 0.0)
7171 }
7172 };
7173 let (barrage_speed, barrage_spread, barrage_count) = {
7174 if let Some(AbilityData::BasicRanged {
7175 projectile_speed,
7176 projectile_spread,
7177 num_projectiles,
7178 ..
7179 }) = self.extract_ability(AbilityInput::Secondary)
7180 {
7181 (
7182 projectile_speed,
7183 projectile_spread,
7184 num_projectiles.compute(self.heads.map_or(1, |heads| heads.amount() as u32)),
7185 )
7186 } else {
7187 (0.0, 0.0, 0)
7188 }
7189 };
7190 let shockwave_range = {
7191 if let Some(AbilityData::Shockwave { range, .. }) =
7192 self.extract_ability(AbilityInput::Auxiliary(0))
7193 {
7194 range
7195 } else {
7196 0.0
7197 }
7198 };
7199
7200 let barrage_max_range =
7202 projectile_flat_range(barrage_speed, self.body.map_or(2.0, |b| b.height()));
7203 let barrange_angle = projectile_multi_angle(barrage_spread, barrage_count);
7204
7205 let is_in_strike_range = attack_data.dist_sqrd
7207 < (attack_data.body_dist + strike_range * STRIKE_RANGE_FACTOR).powi(2);
7208 let is_in_strike_angle = attack_data.angle < strike_angle * STRIKE_AIM_FACTOR;
7209
7210 if !agent.combat_state.initialized {
7212 agent.combat_state.initialized = true;
7213 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7214 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7215 }
7216
7217 match self.char_state {
7222 CharacterState::BasicSummon(s) if s.stage_section == StageSection::Recover => {
7223 agent.combat_state.timers[SUMMON_TOTEM] = 0.0;
7225 agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] = true;
7226 },
7227 CharacterState::Shockwave(_) | CharacterState::BasicRanged(_) => {
7228 agent.combat_state.counters[HEAVY_ATTACK] = 0.0;
7230 agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN] =
7231 rng_from_span(rng, HEAVY_ATTACK_COOLDOWN_SPAN);
7232 },
7233 _ => {},
7234 }
7235 agent.combat_state.timers[SUMMON_TOTEM] += read_data.dt.0;
7237 if is_in_strike_range {
7239 if is_in_strike_angle {
7241 agent.combat_state.counters[HEAVY_ATTACK] += read_data.dt.0;
7242 } else {
7243 agent.combat_state.counters[HEAVY_ATTACK] +=
7245 read_data.dt.0 * HEAVY_ATTACK_FAST_CHARGE_FACTOR;
7246 }
7247 } else {
7248 agent.combat_state.counters[HEAVY_ATTACK] +=
7250 read_data.dt.0 * HEAVY_ATTACK_CHARGE_FACTOR;
7251 }
7252
7253 if !agent.combat_state.conditions[HAS_SUMMONED_FIRST_TOTEM] {
7256 controller.push_basic_input(InputKind::Ability(2));
7257 }
7258 else if agent.combat_state.timers[SUMMON_TOTEM] > TOTEM_COOLDOWN {
7260 controller.push_basic_input(InputKind::Ability(rng.random_range(1..=3)));
7261 }
7262 else if agent.combat_state.counters[HEAVY_ATTACK]
7266 > agent.combat_state.counters[HEAVY_ATTACK_COOLDOWN]
7267 && attack_data.dist_sqrd < (barrage_max_range * BARRAGE_RANGE_FACTOR).powi(2)
7268 {
7269 if line_of_sight_with_target() {
7271 if attack_data.angle > barrange_angle * BARRAGE_AIM_FACTOR {
7273 controller.push_basic_input(InputKind::Ability(0));
7274 }
7275 else if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2)
7277 {
7278 if rng.random_bool(0.5) {
7279 controller.push_basic_input(InputKind::Secondary);
7280 } else {
7281 controller.push_basic_input(InputKind::Ability(0));
7282 }
7283 }
7284 else {
7286 controller.push_basic_input(InputKind::Secondary);
7287 }
7288 }
7290 else {
7292 if attack_data.dist_sqrd < (shockwave_range * SHOCKWAVE_RANGE_FACTOR).powi(2) {
7294 controller.push_basic_input(InputKind::Ability(0));
7295 }
7296 }
7298 }
7299 else if is_in_strike_range && is_in_strike_angle {
7301 controller.push_basic_input(InputKind::Primary);
7302 }
7303 if attack_data.dist_sqrd
7308 > (attack_data.body_dist + strike_range * PATH_RANGE_FACTOR).powi(2)
7309 {
7310 self.path_toward_target(
7311 agent,
7312 controller,
7313 tgt_data.pos.0,
7314 read_data,
7315 Path::AtTarget,
7316 None,
7317 );
7318 }
7319 else if attack_data.angle > 0.0 {
7321 controller.inputs.move_dir = (tgt_data.pos.0 - self.pos.0)
7323 .xy()
7324 .try_normalized()
7325 .unwrap_or_else(Vec2::zero)
7326 * 0.001; }
7328 }
7329
7330 pub fn handle_sword_simple_attack(
7331 &self,
7332 agent: &mut Agent,
7333 controller: &mut Controller,
7334 attack_data: &AttackData,
7335 tgt_data: &TargetData,
7336 read_data: &ReadData,
7337 ) {
7338 const DASH_TIMER: usize = 0;
7339 agent.combat_state.timers[DASH_TIMER] += read_data.dt.0;
7340 if matches!(self.char_state, CharacterState::DashMelee(s) if !matches!(s.stage_section, StageSection::Recover))
7341 {
7342 controller.push_basic_input(InputKind::Secondary);
7343 } else if attack_data.in_min_range() && attack_data.angle < 45.0 {
7344 if agent.combat_state.timers[DASH_TIMER] > 2.0 {
7345 agent.combat_state.timers[DASH_TIMER] = 0.0;
7346 }
7347 controller.push_basic_input(InputKind::Primary);
7348 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2)
7349 && self
7350 .path_toward_target(
7351 agent,
7352 controller,
7353 tgt_data.pos.0,
7354 read_data,
7355 Path::Separate,
7356 None,
7357 )
7358 .is_some()
7359 && entities_have_line_of_sight(
7360 self.pos,
7361 self.body,
7362 self.scale,
7363 tgt_data.pos,
7364 tgt_data.body,
7365 tgt_data.scale,
7366 read_data,
7367 )
7368 && agent.combat_state.timers[DASH_TIMER] > 4.0
7369 && attack_data.angle < 45.0
7370 {
7371 controller.push_basic_input(InputKind::Secondary);
7372 agent.combat_state.timers[DASH_TIMER] = 0.0;
7373 } else {
7374 self.path_toward_target(
7375 agent,
7376 controller,
7377 tgt_data.pos.0,
7378 read_data,
7379 Path::AtTarget,
7380 None,
7381 );
7382 }
7383 }
7384
7385 pub fn handle_adlet_hunter(
7386 &self,
7387 agent: &mut Agent,
7388 controller: &mut Controller,
7389 attack_data: &AttackData,
7390 tgt_data: &TargetData,
7391 read_data: &ReadData,
7392 rng: &mut impl Rng,
7393 ) {
7394 const ROTATE_TIMER: usize = 0;
7395 const ROTATE_DIR_CONDITION: usize = 0;
7396 agent.combat_state.timers[ROTATE_TIMER] -= read_data.dt.0;
7397 if agent.combat_state.timers[ROTATE_TIMER] < 0.0 {
7398 agent.combat_state.conditions[ROTATE_DIR_CONDITION] = rng.random_bool(0.5);
7399 agent.combat_state.timers[ROTATE_TIMER] = rng.random::<f32>() * 5.0;
7400 }
7401 let primary = self.extract_ability(AbilityInput::Primary);
7402 let secondary = self.extract_ability(AbilityInput::Secondary);
7403 let could_use_input = |input| match input {
7404 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7405 p.could_use(
7406 attack_data,
7407 self,
7408 tgt_data,
7409 read_data,
7410 AbilityPreferences::default(),
7411 )
7412 }),
7413 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7414 s.could_use(
7415 attack_data,
7416 self,
7417 tgt_data,
7418 read_data,
7419 AbilityPreferences::default(),
7420 )
7421 }),
7422 _ => false,
7423 };
7424 let move_forwards = if could_use_input(InputKind::Primary) {
7425 controller.push_basic_input(InputKind::Primary);
7426 false
7427 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 8_f32.powi(2) {
7428 controller.push_basic_input(InputKind::Secondary);
7429 true
7430 } else {
7431 true
7432 };
7433
7434 if move_forwards && attack_data.dist_sqrd > 3_f32.powi(2) {
7435 self.path_toward_target(
7436 agent,
7437 controller,
7438 tgt_data.pos.0,
7439 read_data,
7440 Path::Separate,
7441 None,
7442 );
7443 } else {
7444 self.path_toward_target(
7445 agent,
7446 controller,
7447 tgt_data.pos.0,
7448 read_data,
7449 Path::Separate,
7450 None,
7451 );
7452 let dir = if agent.combat_state.conditions[ROTATE_DIR_CONDITION] {
7453 1.0
7454 } else {
7455 -1.0
7456 };
7457 controller.inputs.move_dir.rotate_z(PI / 2.0 * dir);
7458 }
7459 }
7460
7461 pub fn handle_adlet_icepicker(
7462 &self,
7463 agent: &mut Agent,
7464 controller: &mut Controller,
7465 attack_data: &AttackData,
7466 tgt_data: &TargetData,
7467 read_data: &ReadData,
7468 ) {
7469 let primary = self.extract_ability(AbilityInput::Primary);
7470 let secondary = self.extract_ability(AbilityInput::Secondary);
7471 let could_use_input = |input| match input {
7472 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7473 p.could_use(
7474 attack_data,
7475 self,
7476 tgt_data,
7477 read_data,
7478 AbilityPreferences::default(),
7479 )
7480 }),
7481 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7482 s.could_use(
7483 attack_data,
7484 self,
7485 tgt_data,
7486 read_data,
7487 AbilityPreferences::default(),
7488 )
7489 }),
7490 _ => false,
7491 };
7492 let move_forwards = if could_use_input(InputKind::Primary) {
7493 controller.push_basic_input(InputKind::Primary);
7494 false
7495 } else if could_use_input(InputKind::Secondary) && attack_data.dist_sqrd > 5_f32.powi(2) {
7496 controller.push_basic_input(InputKind::Secondary);
7497 false
7498 } else {
7499 true
7500 };
7501
7502 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7503 self.path_toward_target(
7504 agent,
7505 controller,
7506 tgt_data.pos.0,
7507 read_data,
7508 Path::Separate,
7509 None,
7510 );
7511 }
7512 }
7513
7514 pub fn handle_adlet_tracker(
7515 &self,
7516 agent: &mut Agent,
7517 controller: &mut Controller,
7518 attack_data: &AttackData,
7519 tgt_data: &TargetData,
7520 read_data: &ReadData,
7521 ) {
7522 const TRAP_TIMER: usize = 0;
7523 agent.combat_state.timers[TRAP_TIMER] += read_data.dt.0;
7524 if agent.combat_state.timers[TRAP_TIMER] > 20.0 {
7525 agent.combat_state.timers[TRAP_TIMER] = 0.0;
7526 }
7527 let primary = self.extract_ability(AbilityInput::Primary);
7528 let could_use_input = |input| match input {
7529 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7530 p.could_use(
7531 attack_data,
7532 self,
7533 tgt_data,
7534 read_data,
7535 AbilityPreferences::default(),
7536 )
7537 }),
7538 _ => false,
7539 };
7540 let move_forwards = if agent.combat_state.timers[TRAP_TIMER] < 3.0 {
7541 controller.push_basic_input(InputKind::Secondary);
7542 false
7543 } else if could_use_input(InputKind::Primary) {
7544 controller.push_basic_input(InputKind::Primary);
7545 false
7546 } else {
7547 true
7548 };
7549
7550 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7551 self.path_toward_target(
7552 agent,
7553 controller,
7554 tgt_data.pos.0,
7555 read_data,
7556 Path::Separate,
7557 None,
7558 );
7559 }
7560 }
7561
7562 pub fn handle_adlet_elder(
7563 &self,
7564 agent: &mut Agent,
7565 controller: &mut Controller,
7566 attack_data: &AttackData,
7567 tgt_data: &TargetData,
7568 read_data: &ReadData,
7569 rng: &mut impl Rng,
7570 ) {
7571 const TRAP_TIMER: usize = 0;
7572 agent.combat_state.timers[TRAP_TIMER] -= read_data.dt.0;
7573 if matches!(self.char_state, CharacterState::BasicRanged(_)) {
7574 agent.combat_state.timers[TRAP_TIMER] = 15.0;
7575 }
7576 let primary = self.extract_ability(AbilityInput::Primary);
7577 let secondary = self.extract_ability(AbilityInput::Secondary);
7578 let abilities = [
7579 self.extract_ability(AbilityInput::Auxiliary(0)),
7580 self.extract_ability(AbilityInput::Auxiliary(1)),
7581 ];
7582 let could_use_input = |input| match input {
7583 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7584 p.could_use(
7585 attack_data,
7586 self,
7587 tgt_data,
7588 read_data,
7589 AbilityPreferences::default(),
7590 )
7591 }),
7592 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7593 s.could_use(
7594 attack_data,
7595 self,
7596 tgt_data,
7597 read_data,
7598 AbilityPreferences::default(),
7599 )
7600 }),
7601 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7602 a.could_use(
7603 attack_data,
7604 self,
7605 tgt_data,
7606 read_data,
7607 AbilityPreferences::default(),
7608 )
7609 }),
7610 _ => false,
7611 };
7612 let move_forwards = if matches!(self.char_state, CharacterState::DashMelee(s) if s.stage_section != StageSection::Recover)
7613 {
7614 controller.push_basic_input(InputKind::Secondary);
7615 false
7616 } else if agent.combat_state.timers[TRAP_TIMER] < 0.0 && !tgt_data.considered_ranged() {
7617 controller.push_basic_input(InputKind::Ability(0));
7618 false
7619 } else if could_use_input(InputKind::Primary) {
7620 controller.push_basic_input(InputKind::Primary);
7621 false
7622 } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.5) {
7623 controller.push_basic_input(InputKind::Secondary);
7624 false
7625 } else if could_use_input(InputKind::Ability(1)) {
7626 controller.push_basic_input(InputKind::Ability(1));
7627 false
7628 } else {
7629 true
7630 };
7631
7632 if matches!(self.char_state, CharacterState::LeapMelee(_)) {
7633 let tgt_vec = tgt_data.pos.0.xy() - self.pos.0.xy();
7634 if tgt_vec.magnitude_squared() > 2_f32.powi(2)
7635 && let Some(look_dir) = Dir::from_unnormalized(Vec3::from(tgt_vec))
7636 {
7637 controller.inputs.look_dir = look_dir;
7638 }
7639 }
7640
7641 if move_forwards && attack_data.dist_sqrd > 2_f32.powi(2) {
7642 self.path_toward_target(
7643 agent,
7644 controller,
7645 tgt_data.pos.0,
7646 read_data,
7647 Path::Separate,
7648 None,
7649 );
7650 }
7651 }
7652
7653 pub fn handle_icedrake(
7654 &self,
7655 agent: &mut Agent,
7656 controller: &mut Controller,
7657 attack_data: &AttackData,
7658 tgt_data: &TargetData,
7659 read_data: &ReadData,
7660 rng: &mut impl Rng,
7661 ) {
7662 let primary = self.extract_ability(AbilityInput::Primary);
7663 let secondary = self.extract_ability(AbilityInput::Secondary);
7664 let abilities = [
7665 self.extract_ability(AbilityInput::Auxiliary(0)),
7666 self.extract_ability(AbilityInput::Auxiliary(1)),
7667 ];
7668 let could_use_input = |input| match input {
7669 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7670 p.could_use(
7671 attack_data,
7672 self,
7673 tgt_data,
7674 read_data,
7675 AbilityPreferences::default(),
7676 )
7677 }),
7678 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7679 s.could_use(
7680 attack_data,
7681 self,
7682 tgt_data,
7683 read_data,
7684 AbilityPreferences::default(),
7685 )
7686 }),
7687 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7688 a.could_use(
7689 attack_data,
7690 self,
7691 tgt_data,
7692 read_data,
7693 AbilityPreferences::default(),
7694 )
7695 }),
7696 _ => false,
7697 };
7698
7699 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7700 Some(input @ InputKind::Primary) => {
7701 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7702 && could_use_input(input)
7703 {
7704 controller.push_basic_input(input);
7705 true
7706 } else {
7707 false
7708 }
7709 },
7710 Some(input @ InputKind::Ability(1)) => {
7711 if self
7712 .char_state
7713 .timer()
7714 .is_some_and(|t| t.as_secs_f32() < 3.0)
7715 && could_use_input(input)
7716 {
7717 controller.push_basic_input(input);
7718 true
7719 } else {
7720 false
7721 }
7722 },
7723 _ => false,
7724 };
7725
7726 let move_forwards = if !continued_attack {
7727 if could_use_input(InputKind::Primary) && rng.random_bool(0.4) {
7728 controller.push_basic_input(InputKind::Primary);
7729 false
7730 } else if could_use_input(InputKind::Secondary) && rng.random_bool(0.8) {
7731 controller.push_basic_input(InputKind::Secondary);
7732 false
7733 } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(0.9) {
7734 controller.push_basic_input(InputKind::Ability(1));
7735 true
7736 } else if could_use_input(InputKind::Ability(0)) {
7737 controller.push_basic_input(InputKind::Ability(0));
7738 true
7739 } else {
7740 true
7741 }
7742 } else {
7743 false
7744 };
7745
7746 if move_forwards {
7747 self.path_toward_target(
7748 agent,
7749 controller,
7750 tgt_data.pos.0,
7751 read_data,
7752 Path::Separate,
7753 None,
7754 );
7755 }
7756 }
7757
7758 pub fn handle_hydra(
7759 &self,
7760 agent: &mut Agent,
7761 controller: &mut Controller,
7762 attack_data: &AttackData,
7763 tgt_data: &TargetData,
7764 read_data: &ReadData,
7765 rng: &mut impl Rng,
7766 ) {
7767 enum ActionStateTimers {
7768 RegrowHeadNoDamage,
7769 RegrowHeadNoAttack,
7770 }
7771
7772 let could_use_input = |input| {
7773 Option::from(input)
7774 .and_then(|ability| {
7775 Some(self.extract_ability(ability)?.could_use(
7776 attack_data,
7777 self,
7778 tgt_data,
7779 read_data,
7780 AbilityPreferences::default(),
7781 ))
7782 })
7783 .unwrap_or(false)
7784 };
7785
7786 const FOCUS_ATTACK_RANGE: f32 = 5.0;
7787
7788 if attack_data.dist_sqrd < FOCUS_ATTACK_RANGE.powi(2) {
7789 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] = 0.0;
7790 } else {
7791 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize] +=
7792 read_data.dt.0;
7793 }
7794
7795 if let Some(health) = self.health.filter(|health| health.last_change.amount < 0.0) {
7796 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] =
7797 (read_data.time.0 - health.last_change.time.0) as f32;
7798 } else {
7799 agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] +=
7800 read_data.dt.0;
7801 }
7802
7803 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
7804 match self.char_state {
7805 CharacterState::ChargedMelee(c) => {
7806 if c.charge_frac() < 1.0 && could_use_input(input) {
7807 controller.push_basic_input(input);
7808 }
7809 },
7810 CharacterState::ChargedRanged(c) => {
7811 if c.charge_frac() < 1.0 && could_use_input(input) {
7812 controller.push_basic_input(input);
7813 }
7814 },
7815 _ => {},
7816 }
7817 }
7818
7819 let continued_attack = match self.char_state.ability_info().map(|ai| ai.input) {
7820 Some(input @ InputKind::Primary) => {
7821 if !matches!(self.char_state.stage_section(), Some(StageSection::Recover))
7822 && could_use_input(input)
7823 {
7824 controller.push_basic_input(input);
7825 true
7826 } else {
7827 false
7828 }
7829 },
7830 _ => false,
7831 };
7832
7833 let has_heads = self.heads.is_none_or(|heads| heads.amount() > 0);
7834
7835 let move_forwards = if !continued_attack {
7836 if could_use_input(InputKind::Ability(1))
7837 && rng.random_bool(0.9)
7838 && (agent.combat_state.timers[ActionStateTimers::RegrowHeadNoDamage as usize] > 5.0
7839 || agent.combat_state.timers[ActionStateTimers::RegrowHeadNoAttack as usize]
7840 > 6.0)
7841 && self.heads.is_some_and(|heads| heads.amount_missing() > 0)
7842 {
7843 controller.push_basic_input(InputKind::Ability(2));
7844 false
7845 } else if has_heads && could_use_input(InputKind::Primary) && rng.random_bool(0.8) {
7846 controller.push_basic_input(InputKind::Primary);
7847 true
7848 } else if has_heads && could_use_input(InputKind::Secondary) && rng.random_bool(0.4) {
7849 controller.push_basic_input(InputKind::Secondary);
7850 false
7851 } else if has_heads && could_use_input(InputKind::Ability(1)) && rng.random_bool(0.6) {
7852 controller.push_basic_input(InputKind::Ability(1));
7853 true
7854 } else if !has_heads && could_use_input(InputKind::Ability(3)) && rng.random_bool(0.7) {
7855 controller.push_basic_input(InputKind::Ability(3));
7856 true
7857 } else if could_use_input(InputKind::Ability(0)) {
7858 controller.push_basic_input(InputKind::Ability(0));
7859 true
7860 } else {
7861 true
7862 }
7863 } else {
7864 true
7865 };
7866
7867 if move_forwards {
7868 if has_heads {
7869 self.path_toward_target(
7870 agent,
7871 controller,
7872 tgt_data.pos.0,
7873 read_data,
7874 Path::Separate,
7875 (attack_data.dist_sqrd
7877 < (2.5 + self.body.map_or(0.0, |b| b.front_radius())).powi(2))
7878 .then_some(0.3),
7879 );
7880 } else {
7881 self.flee(agent, controller, read_data, tgt_data.pos);
7882 }
7883 }
7884 }
7885
7886 pub fn handle_random_abilities(
7887 &self,
7888 agent: &mut Agent,
7889 controller: &mut Controller,
7890 attack_data: &AttackData,
7891 tgt_data: &TargetData,
7892 read_data: &ReadData,
7893 rng: &mut impl Rng,
7894 primary_weight: u8,
7895 secondary_weight: u8,
7896 ability_weights: [u8; BASE_ABILITY_LIMIT],
7897 ) {
7898 let primary = self.extract_ability(AbilityInput::Primary);
7899 let secondary = self.extract_ability(AbilityInput::Secondary);
7900 let abilities = [
7901 self.extract_ability(AbilityInput::Auxiliary(0)),
7902 self.extract_ability(AbilityInput::Auxiliary(1)),
7903 self.extract_ability(AbilityInput::Auxiliary(2)),
7904 self.extract_ability(AbilityInput::Auxiliary(3)),
7905 self.extract_ability(AbilityInput::Auxiliary(4)),
7906 ];
7907 let could_use_input = |input| match input {
7908 InputKind::Primary => primary.as_ref().is_some_and(|p| {
7909 p.could_use(
7910 attack_data,
7911 self,
7912 tgt_data,
7913 read_data,
7914 AbilityPreferences::default(),
7915 )
7916 }),
7917 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
7918 s.could_use(
7919 attack_data,
7920 self,
7921 tgt_data,
7922 read_data,
7923 AbilityPreferences::default(),
7924 )
7925 }),
7926 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
7927 a.could_use(
7928 attack_data,
7929 self,
7930 tgt_data,
7931 read_data,
7932 AbilityPreferences::default(),
7933 )
7934 }),
7935 _ => false,
7936 };
7937
7938 let primary_chance = primary_weight as f64
7939 / ((primary_weight + secondary_weight + ability_weights.iter().sum::<u8>()) as f64)
7940 .max(0.01);
7941 let secondary_chance = secondary_weight as f64
7942 / ((secondary_weight + ability_weights.iter().sum::<u8>()) as f64).max(0.01);
7943 let ability_chances = {
7944 let mut chances = [0.0; BASE_ABILITY_LIMIT];
7945 chances.iter_mut().enumerate().for_each(|(i, chance)| {
7946 *chance = ability_weights[i] as f64
7947 / (ability_weights
7948 .iter()
7949 .enumerate()
7950 .filter_map(|(j, weight)| if j >= i { Some(weight) } else { None })
7951 .sum::<u8>() as f64)
7952 .max(0.01)
7953 });
7954 chances
7955 };
7956
7957 if let Some(input) = self.char_state.ability_info().map(|ai| ai.input) {
7958 match self.char_state {
7959 CharacterState::ChargedMelee(c) => {
7960 if c.charge_frac() < 1.0 && could_use_input(input) {
7961 controller.push_basic_input(input);
7962 }
7963 },
7964 CharacterState::ChargedRanged(c) => {
7965 if c.charge_frac() < 1.0 && could_use_input(input) {
7966 controller.push_basic_input(input);
7967 }
7968 },
7969 _ => {},
7970 }
7971 }
7972
7973 let move_forwards = if could_use_input(InputKind::Primary)
7974 && rng.random_bool(primary_chance)
7975 {
7976 controller.push_basic_input(InputKind::Primary);
7977 false
7978 } else if could_use_input(InputKind::Secondary) && rng.random_bool(secondary_chance) {
7979 controller.push_basic_input(InputKind::Secondary);
7980 false
7981 } else if could_use_input(InputKind::Ability(0)) && rng.random_bool(ability_chances[0]) {
7982 controller.push_basic_input(InputKind::Ability(0));
7983 false
7984 } else if could_use_input(InputKind::Ability(1)) && rng.random_bool(ability_chances[1]) {
7985 controller.push_basic_input(InputKind::Ability(1));
7986 false
7987 } else if could_use_input(InputKind::Ability(2)) && rng.random_bool(ability_chances[2]) {
7988 controller.push_basic_input(InputKind::Ability(2));
7989 false
7990 } else if could_use_input(InputKind::Ability(3)) && rng.random_bool(ability_chances[3]) {
7991 controller.push_basic_input(InputKind::Ability(3));
7992 false
7993 } else if could_use_input(InputKind::Ability(4)) && rng.random_bool(ability_chances[4]) {
7994 controller.push_basic_input(InputKind::Ability(4));
7995 false
7996 } else {
7997 true
7998 };
7999
8000 if move_forwards {
8001 self.path_toward_target(
8002 agent,
8003 controller,
8004 tgt_data.pos.0,
8005 read_data,
8006 Path::Separate,
8007 None,
8008 );
8009 }
8010 }
8011
8012 pub fn handle_simple_double_attack(
8013 &self,
8014 agent: &mut Agent,
8015 controller: &mut Controller,
8016 attack_data: &AttackData,
8017 tgt_data: &TargetData,
8018 read_data: &ReadData,
8019 ) {
8020 const MAX_ATTACK_RANGE: f32 = 20.0;
8021
8022 if attack_data.angle < 60.0 && attack_data.dist_sqrd < MAX_ATTACK_RANGE.powi(2) {
8023 controller.inputs.move_dir = Vec2::zero();
8024 if attack_data.in_min_range() {
8025 controller.push_basic_input(InputKind::Primary);
8026 } else {
8027 controller.push_basic_input(InputKind::Secondary);
8028 }
8029 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8030 self.path_toward_target(
8031 agent,
8032 controller,
8033 tgt_data.pos.0,
8034 read_data,
8035 Path::Separate,
8036 None,
8037 );
8038 } else {
8039 self.path_toward_target(
8040 agent,
8041 controller,
8042 tgt_data.pos.0,
8043 read_data,
8044 Path::AtTarget,
8045 None,
8046 );
8047 }
8048 }
8049
8050 pub fn handle_clay_steed_attack(
8051 &self,
8052 agent: &mut Agent,
8053 controller: &mut Controller,
8054 attack_data: &AttackData,
8055 tgt_data: &TargetData,
8056 read_data: &ReadData,
8057 ) {
8058 enum ActionStateTimers {
8059 AttackTimer,
8060 }
8061 const HOOF_ATTACK_RANGE: f32 = 1.0;
8062 const HOOF_ATTACK_ANGLE: f32 = 50.0;
8063
8064 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8065 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 10.0 {
8066 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8068 }
8069
8070 if attack_data.angle < HOOF_ATTACK_ANGLE
8071 && attack_data.dist_sqrd
8072 < (HOOF_ATTACK_RANGE + self.body.map_or(0.0, |b| b.max_radius())).powi(2)
8073 {
8074 controller.inputs.move_dir = Vec2::zero();
8075 controller.push_basic_input(InputKind::Primary);
8076 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 5.0 {
8077 controller.push_basic_input(InputKind::Secondary);
8078 } else {
8079 self.path_toward_target(
8080 agent,
8081 controller,
8082 tgt_data.pos.0,
8083 read_data,
8084 Path::AtTarget,
8085 None,
8086 );
8087 }
8088 }
8089
8090 pub fn handle_ancient_effigy_attack(
8091 &self,
8092 agent: &mut Agent,
8093 controller: &mut Controller,
8094 attack_data: &AttackData,
8095 tgt_data: &TargetData,
8096 read_data: &ReadData,
8097 ) {
8098 enum ActionStateTimers {
8099 BlastTimer,
8100 }
8101
8102 let home = agent.patrol_origin.unwrap_or(self.pos.0);
8103 let line_of_sight_with_target = || {
8104 entities_have_line_of_sight(
8105 self.pos,
8106 self.body,
8107 self.scale,
8108 tgt_data.pos,
8109 tgt_data.body,
8110 tgt_data.scale,
8111 read_data,
8112 )
8113 };
8114 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] += read_data.dt.0;
8115
8116 if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] > 6.0 {
8117 agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] = 0.0;
8118 }
8119 if line_of_sight_with_target() {
8120 if attack_data.in_min_range() {
8121 controller.push_basic_input(InputKind::Secondary);
8122 } else if agent.combat_state.timers[ActionStateTimers::BlastTimer as usize] < 2.0 {
8123 controller.push_basic_input(InputKind::Primary);
8124 } else {
8125 self.path_toward_target(
8126 agent,
8127 controller,
8128 tgt_data.pos.0,
8129 read_data,
8130 Path::Separate,
8131 None,
8132 );
8133 }
8134 } else {
8135 if (home - self.pos.0).xy().magnitude_squared() > (3.0_f32).powi(2) {
8137 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8138 }
8139 }
8140 }
8141
8142 pub fn handle_clay_golem_attack(
8143 &self,
8144 agent: &mut Agent,
8145 controller: &mut Controller,
8146 attack_data: &AttackData,
8147 tgt_data: &TargetData,
8148 read_data: &ReadData,
8149 ) {
8150 const MIN_DASH_RANGE: f32 = 15.0;
8151
8152 enum ActionStateTimers {
8153 AttackTimer,
8154 }
8155
8156 let line_of_sight_with_target = || {
8157 entities_have_line_of_sight(
8158 self.pos,
8159 self.body,
8160 self.scale,
8161 tgt_data.pos,
8162 tgt_data.body,
8163 tgt_data.scale,
8164 read_data,
8165 )
8166 };
8167 let spawn = agent.patrol_origin.unwrap_or(self.pos.0);
8168 let home = Vec3::new(spawn.x - 32.0, spawn.y - 12.0, spawn.z);
8169 let is_home = (home - self.pos.0).xy().magnitude_squared() < (3.0_f32).powi(2);
8170 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] += read_data.dt.0;
8171 if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] > 8.0 {
8172 agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] = 0.0;
8174 }
8175 if line_of_sight_with_target() {
8176 controller.inputs.move_dir = Vec2::zero();
8177 if attack_data.in_min_range() {
8178 controller.push_basic_input(InputKind::Primary);
8179 } else if attack_data.dist_sqrd > MIN_DASH_RANGE.powi(2) {
8180 controller.push_basic_input(InputKind::Secondary);
8181 } else {
8182 self.path_toward_target(
8183 agent,
8184 controller,
8185 tgt_data.pos.0,
8186 read_data,
8187 Path::AtTarget,
8188 None,
8189 );
8190 }
8191 } else if agent.combat_state.timers[ActionStateTimers::AttackTimer as usize] < 4.0 {
8192 if !is_home {
8193 self.path_toward_target(agent, controller, home, read_data, Path::Separate, None);
8195 } else {
8196 self.path_toward_target(agent, controller, spawn, read_data, Path::Separate, None);
8197 }
8198 } else if attack_data.dist_sqrd < MAX_PATH_DIST.powi(2) {
8199 self.path_toward_target(
8200 agent,
8201 controller,
8202 tgt_data.pos.0,
8203 read_data,
8204 Path::Separate,
8205 None,
8206 );
8207 }
8208 }
8209
8210 pub fn handle_haniwa_soldier(
8211 &self,
8212 agent: &mut Agent,
8213 controller: &mut Controller,
8214 attack_data: &AttackData,
8215 tgt_data: &TargetData,
8216 read_data: &ReadData,
8217 ) {
8218 const DEFENSIVE_CONDITION: usize = 0;
8219 const RIPOSTE_TIMER: usize = 0;
8220 const MODE_CYCLE_TIMER: usize = 1;
8221
8222 let primary = self.extract_ability(AbilityInput::Primary);
8223 let secondary = self.extract_ability(AbilityInput::Secondary);
8224 let could_use_input = |input| match input {
8225 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8226 p.could_use(
8227 attack_data,
8228 self,
8229 tgt_data,
8230 read_data,
8231 AbilityPreferences::default(),
8232 )
8233 }),
8234 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8235 s.could_use(
8236 attack_data,
8237 self,
8238 tgt_data,
8239 read_data,
8240 AbilityPreferences::default(),
8241 )
8242 }),
8243 _ => false,
8244 };
8245
8246 agent.combat_state.timers[RIPOSTE_TIMER] += read_data.dt.0;
8247 agent.combat_state.timers[MODE_CYCLE_TIMER] += read_data.dt.0;
8248
8249 if agent.combat_state.timers[MODE_CYCLE_TIMER] > 7.0 {
8250 agent.combat_state.conditions[DEFENSIVE_CONDITION] =
8251 !agent.combat_state.conditions[DEFENSIVE_CONDITION];
8252 agent.combat_state.timers[MODE_CYCLE_TIMER] = 0.0;
8253 }
8254
8255 if matches!(self.char_state, CharacterState::RiposteMelee(_)) {
8256 agent.combat_state.timers[RIPOSTE_TIMER] = 0.0;
8257 }
8258
8259 let try_move = if agent.combat_state.conditions[DEFENSIVE_CONDITION] {
8260 controller.push_basic_input(InputKind::Block);
8261 true
8262 } else if agent.combat_state.timers[RIPOSTE_TIMER] > 10.0
8263 && could_use_input(InputKind::Secondary)
8264 {
8265 controller.push_basic_input(InputKind::Secondary);
8266 false
8267 } else if could_use_input(InputKind::Primary) {
8268 controller.push_basic_input(InputKind::Primary);
8269 false
8270 } else {
8271 true
8272 };
8273
8274 if try_move && attack_data.dist_sqrd > 2_f32.powi(2) {
8275 self.path_toward_target(
8276 agent,
8277 controller,
8278 tgt_data.pos.0,
8279 read_data,
8280 Path::Separate,
8281 None,
8282 );
8283 }
8284 }
8285
8286 pub fn handle_haniwa_guard(
8287 &self,
8288 agent: &mut Agent,
8289 controller: &mut Controller,
8290 attack_data: &AttackData,
8291 tgt_data: &TargetData,
8292 read_data: &ReadData,
8293 rng: &mut impl Rng,
8294 ) {
8295 const BACKPEDAL_DIST: f32 = 5.0;
8296 const ROTATE_CCW_CONDITION: usize = 0;
8297 const FLURRY_TIMER: usize = 0;
8298 const BACKPEDAL_TIMER: usize = 1;
8299 const SWITCH_ROTATE_TIMER: usize = 2;
8300 const SWITCH_ROTATE_COUNTER: usize = 0;
8301
8302 let primary = self.extract_ability(AbilityInput::Primary);
8303 let secondary = self.extract_ability(AbilityInput::Secondary);
8304 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8305 let could_use_input = |input| match input {
8306 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8307 p.could_use(
8308 attack_data,
8309 self,
8310 tgt_data,
8311 read_data,
8312 AbilityPreferences::default(),
8313 )
8314 }),
8315 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8316 s.could_use(
8317 attack_data,
8318 self,
8319 tgt_data,
8320 read_data,
8321 AbilityPreferences::default(),
8322 )
8323 }),
8324 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8325 a.could_use(
8326 attack_data,
8327 self,
8328 tgt_data,
8329 read_data,
8330 AbilityPreferences::default(),
8331 )
8332 }),
8333 _ => false,
8334 };
8335
8336 if !agent.combat_state.initialized {
8337 agent.combat_state.conditions[ROTATE_CCW_CONDITION] = rng.random_bool(0.5);
8338 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8339 agent.combat_state.initialized = true;
8340 }
8341
8342 let continue_flurry = match self.char_state {
8343 CharacterState::BasicMelee(_) => {
8344 agent.combat_state.timers[FLURRY_TIMER] += read_data.dt.0;
8345 false
8346 },
8347 CharacterState::RapidMelee(c) => {
8348 agent.combat_state.timers[FLURRY_TIMER] = 0.0;
8349 !matches!(c.stage_section, StageSection::Recover)
8350 },
8351 CharacterState::ComboMelee2(_) => {
8352 agent.combat_state.timers[BACKPEDAL_TIMER] = 0.0;
8353 false
8354 },
8355 _ => false,
8356 };
8357 agent.combat_state.timers[SWITCH_ROTATE_TIMER] += read_data.dt.0;
8358 agent.combat_state.timers[BACKPEDAL_TIMER] += read_data.dt.0;
8359
8360 if agent.combat_state.timers[SWITCH_ROTATE_TIMER]
8361 > agent.combat_state.counters[SWITCH_ROTATE_COUNTER]
8362 {
8363 agent.combat_state.conditions[ROTATE_CCW_CONDITION] =
8364 !agent.combat_state.conditions[ROTATE_CCW_CONDITION];
8365 agent.combat_state.counters[SWITCH_ROTATE_COUNTER] = rng.random_range(5.0..20.0);
8366 }
8367
8368 let move_farther = attack_data.dist_sqrd < BACKPEDAL_DIST.powi(2);
8369 let move_closer = if continue_flurry && could_use_input(InputKind::Secondary) {
8370 controller.push_basic_input(InputKind::Secondary);
8371 false
8372 } else if agent.combat_state.timers[BACKPEDAL_TIMER] > 10.0
8373 && move_farther
8374 && could_use_input(InputKind::Ability(0))
8375 {
8376 controller.push_basic_input(InputKind::Ability(0));
8377 false
8378 } else if agent.combat_state.timers[FLURRY_TIMER] > 6.0
8379 && could_use_input(InputKind::Secondary)
8380 {
8381 controller.push_basic_input(InputKind::Secondary);
8382 false
8383 } else if could_use_input(InputKind::Primary) {
8384 controller.push_basic_input(InputKind::Primary);
8385 false
8386 } else {
8387 true
8388 };
8389
8390 if let Some((bearing, speed, stuck)) = agent.chaser.chase(
8391 &*read_data.terrain,
8392 self.pos.0,
8393 self.vel.0,
8394 tgt_data.pos.0,
8395 TraversalConfig {
8396 min_tgt_dist: 1.25,
8397 ..self.traversal_config
8398 },
8399 &read_data.time,
8400 ) {
8401 self.unstuck_if(stuck, controller);
8402 if entities_have_line_of_sight(
8403 self.pos,
8404 self.body,
8405 self.scale,
8406 tgt_data.pos,
8407 tgt_data.body,
8408 tgt_data.scale,
8409 read_data,
8410 ) && attack_data.angle < 45.0
8411 {
8412 let angle = match (
8413 agent.combat_state.conditions[ROTATE_CCW_CONDITION],
8414 move_closer,
8415 move_farther,
8416 ) {
8417 (true, true, false) => rng.random_range(-1.5..-0.5),
8418 (true, false, true) => rng.random_range(-2.2..-1.7),
8419 (true, _, _) => rng.random_range(-1.7..-1.5),
8420 (false, true, false) => rng.random_range(0.5..1.5),
8421 (false, false, true) => rng.random_range(1.7..2.2),
8422 (false, _, _) => rng.random_range(1.5..1.7),
8423 };
8424 controller.inputs.move_dir = bearing
8425 .xy()
8426 .rotated_z(angle)
8427 .try_normalized()
8428 .unwrap_or_else(Vec2::zero)
8429 * speed;
8430 } else {
8431 controller.inputs.move_dir =
8432 bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) * speed;
8433 self.jump_if(bearing.z > 1.5, controller);
8434 }
8435 }
8436 }
8437
8438 pub fn handle_haniwa_archer(
8439 &self,
8440 agent: &mut Agent,
8441 controller: &mut Controller,
8442 attack_data: &AttackData,
8443 tgt_data: &TargetData,
8444 read_data: &ReadData,
8445 ) {
8446 const KICK_TIMER: usize = 0;
8447 const EXPLOSIVE_TIMER: usize = 1;
8448
8449 let primary = self.extract_ability(AbilityInput::Primary);
8450 let secondary = self.extract_ability(AbilityInput::Secondary);
8451 let abilities = [self.extract_ability(AbilityInput::Auxiliary(0))];
8452 let could_use_input = |input| match input {
8453 InputKind::Primary => primary.as_ref().is_some_and(|p| {
8454 p.could_use(
8455 attack_data,
8456 self,
8457 tgt_data,
8458 read_data,
8459 AbilityPreferences::default(),
8460 )
8461 }),
8462 InputKind::Secondary => secondary.as_ref().is_some_and(|s| {
8463 s.could_use(
8464 attack_data,
8465 self,
8466 tgt_data,
8467 read_data,
8468 AbilityPreferences::default(),
8469 )
8470 }),
8471 InputKind::Ability(x) => abilities[x].as_ref().is_some_and(|a| {
8472 a.could_use(
8473 attack_data,
8474 self,
8475 tgt_data,
8476 read_data,
8477 AbilityPreferences::default(),
8478 )
8479 }),
8480 _ => false,
8481 };
8482
8483 agent.combat_state.timers[KICK_TIMER] += read_data.dt.0;
8484 agent.combat_state.timers[EXPLOSIVE_TIMER] += read_data.dt.0;
8485
8486 match self.char_state.ability_info().map(|ai| ai.input) {
8487 Some(InputKind::Secondary) => {
8488 agent.combat_state.timers[KICK_TIMER] = 0.0;
8489 },
8490 Some(InputKind::Ability(0)) => {
8491 agent.combat_state.timers[EXPLOSIVE_TIMER] = 0.0;
8492 },
8493 _ => {},
8494 }
8495
8496 if agent.combat_state.timers[KICK_TIMER] > 4.0 && could_use_input(InputKind::Secondary) {
8497 controller.push_basic_input(InputKind::Secondary);
8498 } else if agent.combat_state.timers[EXPLOSIVE_TIMER] > 15.0
8499 && could_use_input(InputKind::Ability(0))
8500 {
8501 controller.push_basic_input(InputKind::Ability(0));
8502 } else if could_use_input(InputKind::Primary) {
8503 controller.push_basic_input(InputKind::Primary);
8504 } else {
8505 self.path_toward_target(
8506 agent,
8507 controller,
8508 tgt_data.pos.0,
8509 read_data,
8510 Path::Separate,
8511 None,
8512 );
8513 }
8514 }
8515
8516 pub fn handle_terracotta_statue_attack(
8517 &self,
8518 agent: &mut Agent,
8519 controller: &mut Controller,
8520 attack_data: &AttackData,
8521 read_data: &ReadData,
8522 ) {
8523 enum Conditions {
8524 AttackToggle,
8525 }
8526 let home = agent.patrol_origin.unwrap_or(self.pos.0.round());
8527 if (home - self.pos.0).xy().magnitude_squared() > (2.0_f32).powi(2) {
8529 self.path_toward_target(agent, controller, home, read_data, Path::AtTarget, None);
8530 } else if !agent.combat_state.conditions[Conditions::AttackToggle as usize] {
8531 controller.push_basic_input(InputKind::Primary);
8533 } else {
8534 controller.inputs.move_dir = Vec2::zero();
8535 if attack_data.dist_sqrd < 8.5f32.powi(2) {
8536 controller.push_basic_input(InputKind::Primary);
8538 } else {
8539 controller.push_basic_input(InputKind::Secondary);
8541 }
8542 }
8543 if matches!(self.char_state, CharacterState::SpriteSummon(c) if matches!(c.stage_section, StageSection::Recover))
8544 {
8545 agent.combat_state.conditions[Conditions::AttackToggle as usize] = true;
8546 }
8547 }
8548
8549 pub fn handle_jiangshi_attack(
8550 &self,
8551 agent: &mut Agent,
8552 controller: &mut Controller,
8553 attack_data: &AttackData,
8554 tgt_data: &TargetData,
8555 read_data: &ReadData,
8556 ) {
8557 if tgt_data.pos.0.z - self.pos.0.z > 5.0 {
8558 controller.push_action(ControlAction::StartInput {
8559 input: InputKind::Secondary,
8560 target_entity: agent
8561 .target
8562 .as_ref()
8563 .and_then(|t| read_data.uids.get(t.target))
8564 .copied(),
8565 select_pos: None,
8566 });
8567 } else if attack_data.dist_sqrd < 12.0f32.powi(2) {
8568 controller.push_basic_input(InputKind::Primary);
8569 }
8570
8571 self.path_toward_target(
8572 agent,
8573 controller,
8574 tgt_data.pos.0,
8575 read_data,
8576 Path::AtTarget,
8577 None,
8578 );
8579 }
8580}