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