use bevy::prelude::*;
use crate::components::*;
use crate::start_attack;
pub fn aabb_overlap(center_a: Vec2, half_a: Vec2, center_b: Vec2, half_b: Vec2) -> bool {
(center_a.x - center_b.x).abs() < half_a.x + half_b.x
&& (center_a.y - center_b.y).abs() < half_a.y + half_b.y
}
pub fn tick_cooldowns(time: Res<Time>, mut q: Query<&mut AttackCooldown>) {
for mut cd in &mut q {
cd.0 = (cd.0 - time.delta_secs()).max(0.0);
}
}
pub fn ai_behavior(
time: Res<Time>,
assets: Res<FighterAssets>,
config: Res<AiDuelConfig>,
mut ai_q: Query<
(
&mut Transform,
&mut CurrentState,
&mut AnimationTimer,
&mut Sprite,
&mut WantsToMove,
&mut AttackCooldown,
&AiController,
),
Without<Player>,
>,
player_q: Query<&Transform, With<Player>>,
) {
let Ok(player_tf) = player_q.single() else {
return;
};
let player_pos = player_tf.translation.truncate();
for (mut tf, mut state, mut anim, mut sprite, mut wants, mut cd, ai) in &mut ai_q {
if state.0 == FighterState::Attacking {
wants.0 = false;
continue;
}
let to_player = player_pos - tf.translation.truncate();
let dist = to_player.length();
let dir = to_player.normalize_or_zero();
if dir.x.abs() > 0.1 {
tf.scale.x = config.sprite_scale * dir.x.signum();
}
if dist < ai.aggression_range && cd.0 <= 0.0 {
start_attack(&mut state, &mut anim, &mut sprite, &assets, &config);
cd.0 = config.attack_cooldown;
wants.0 = false;
continue;
}
if dist > ai.stop_range {
let step = dir * ai.speed * time.delta_secs();
tf.translation.x += step.x;
tf.translation.y += step.y;
wants.0 = true;
} else {
wants.0 = false;
}
}
}
pub fn animate_fighters(
time: Res<Time>,
assets: Res<FighterAssets>,
config: Res<AiDuelConfig>,
mut q: Query<(
&mut AnimationTimer,
&mut Sprite,
&mut CurrentState,
&WantsToMove,
)>,
) {
for (mut anim, mut sprite, mut state, wants) in &mut q {
anim.timer.tick(time.delta());
if !anim.timer.just_finished() {
continue;
}
anim.frame_index += 1;
if state.0 == FighterState::Attacking {
if anim.frame_index >= anim.max_frames {
state.0 = FighterState::Running;
anim.frame_index = 0;
anim.max_frames = config.run_frames.max(1);
anim.timer = Timer::from_seconds(config.run_frame_time, TimerMode::Repeating);
if let Some(atlas) = &mut sprite.texture_atlas {
atlas.layout = assets.run_layout.clone();
atlas.index = 0;
}
sprite.image = assets.run_sheet.clone();
} else if let Some(atlas) = &mut sprite.texture_atlas {
atlas.index = anim.frame_index;
}
} else if wants.0 {
anim.frame_index %= anim.max_frames.max(1);
if let Some(atlas) = &mut sprite.texture_atlas {
atlas.index = anim.frame_index;
}
}
}
}
pub fn update_hitboxes(
config: Res<AiDuelConfig>,
mut hitbox_q: Query<&mut Hitbox>,
fighter_q: Query<(&CurrentState, &AnimationTimer)>,
) {
for mut hb in &mut hitbox_q {
let Ok((state, anim)) = fighter_q.get(hb.owner) else {
continue;
};
hb.active = state.0 == FighterState::Attacking
&& (config.hitbox_active_from..=config.hitbox_active_to).contains(&anim.frame_index);
}
}
pub fn resolve_hits(
config: Res<AiDuelConfig>,
hitbox_q: Query<(&Hitbox, &GlobalTransform)>,
mut target_q: Query<(&GlobalTransform, &Faction, &Hurtbox, &mut Health)>,
) {
for (hb, hb_tf) in &hitbox_q {
if !hb.active {
continue;
}
let center = hb_tf.translation().truncate();
for (tf, faction, hurtbox, mut hp) in &mut target_q {
if *faction == hb.faction {
continue; }
let target = tf.translation().truncate();
if aabb_overlap(center, hb.half_size, target, hurtbox.half_size)
&& hp.try_hit(hb.damage, config.hit_cooldown)
{
debug!("Touché ! PV restants : {:.0}/{:.0}", hp.current, hp.max);
}
}
}
}
pub fn tick_health_and_flash(time: Res<Time>, mut q: Query<(&mut Health, &mut Sprite)>) {
for (mut hp, mut sprite) in &mut q {
if hp.hit_cooldown > 0.0 {
hp.hit_cooldown = (hp.hit_cooldown - time.delta_secs()).max(0.0);
sprite.color = Color::srgba(1.0, 0.3, 0.3, 0.85);
} else {
sprite.color = Color::WHITE;
}
}
}
pub fn check_death(mut commands: Commands, q: Query<(Entity, &Health)>) {
for (entity, hp) in &q {
if hp.is_dead() {
debug!("Un combattant est tombé !");
commands.entity(entity).despawn();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overlapping_boxes_are_detected() {
assert!(aabb_overlap(Vec2::ZERO, Vec2::splat(10.0), Vec2::new(15.0, 0.0), Vec2::splat(10.0)));
}
#[test]
fn distant_boxes_do_not_overlap() {
assert!(!aabb_overlap(Vec2::ZERO, Vec2::splat(10.0), Vec2::new(25.0, 0.0), Vec2::splat(10.0)));
assert!(!aabb_overlap(Vec2::ZERO, Vec2::splat(10.0), Vec2::new(0.0, 25.0), Vec2::splat(10.0)));
}
#[test]
fn touching_edges_do_not_count() {
assert!(!aabb_overlap(Vec2::ZERO, Vec2::splat(10.0), Vec2::new(20.0, 0.0), Vec2::splat(10.0)));
}
}