use bevy::prelude::*;
#[derive(Resource, Clone, Debug)]
pub struct AiDuelConfig {
pub run_frames: usize,
pub run_frame_time: f32,
pub attack_frames: usize,
pub attack_frame_time: f32,
pub attack_cooldown: f32,
pub hit_cooldown: f32,
pub attack_damage: f32,
pub hitbox_active_from: usize,
pub hitbox_active_to: usize,
pub hitbox_half_size: Vec2,
pub hitbox_offset: Vec2,
pub hurtbox_half_size: Vec2,
pub debug_hitboxes: bool,
pub default_max_health: f32,
pub sprite_scale: f32,
}
impl Default for AiDuelConfig {
fn default() -> Self {
Self {
run_frames: 8,
run_frame_time: 0.10,
attack_frames: 6,
attack_frame_time: 0.08,
attack_cooldown: 0.60,
hit_cooldown: 0.40,
attack_damage: 10.0,
hitbox_active_from: 2,
hitbox_active_to: 4,
hitbox_half_size: Vec2::new(35.0, 30.0),
hitbox_offset: Vec2::new(45.0, 0.0),
hurtbox_half_size: Vec2::new(20.0, 35.0),
debug_hitboxes: false,
default_max_health: 100.0,
sprite_scale: 1.5,
}
}
}
#[derive(Resource, Clone, Debug)]
pub struct FighterAssets {
pub run_sheet: Handle<Image>,
pub run_layout: Handle<TextureAtlasLayout>,
pub attack_sheet: Handle<Image>,
pub attack_layout: Handle<TextureAtlasLayout>,
}
#[derive(Component, Debug)]
pub struct Player;
#[derive(Component, PartialEq, Eq, Clone, Copy, Debug)]
pub enum Faction {
Player,
Enemy,
}
#[derive(Component, Debug)]
pub struct Health {
pub current: f32,
pub max: f32,
pub hit_cooldown: f32,
}
impl Health {
pub fn full(max: f32) -> Self {
Self { current: max, max, hit_cooldown: 0.0 }
}
pub fn is_dead(&self) -> bool {
self.current <= 0.0
}
pub fn fraction(&self) -> f32 {
if self.max <= 0.0 {
0.0
} else {
(self.current / self.max).clamp(0.0, 1.0)
}
}
pub fn try_hit(&mut self, damage: f32, invulnerability: f32) -> bool {
if self.is_dead() || self.hit_cooldown > 0.0 {
return false;
}
self.current = (self.current - damage).max(0.0);
self.hit_cooldown = invulnerability;
true
}
}
#[derive(Component, Debug)]
pub struct AttackCooldown(pub f32);
#[derive(Component, Debug)]
pub struct Hurtbox {
pub half_size: Vec2,
}
#[derive(Component, Debug)]
pub struct Hitbox {
pub owner: Entity,
pub faction: Faction,
pub half_size: Vec2,
pub damage: f32,
pub active: bool,
}
#[derive(Component, Clone, Debug)]
pub struct AiController {
pub aggression_range: f32,
pub stop_range: f32,
pub speed: f32,
}
impl Default for AiController {
fn default() -> Self {
Self { aggression_range: 90.0, stop_range: 55.0, speed: 180.0 }
}
}
#[derive(Component, Debug)]
pub struct WantsToMove(pub bool);
#[derive(Component, Debug)]
pub struct AnimationTimer {
pub timer: Timer,
pub frame_index: usize,
pub max_frames: usize,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum FighterState {
Running,
Attacking,
}
#[derive(Component, Debug)]
pub struct CurrentState(pub FighterState);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn health_starts_full() {
let hp = Health::full(100.0);
assert_eq!(hp.current, 100.0);
assert!(!hp.is_dead());
assert_eq!(hp.fraction(), 1.0);
}
#[test]
fn try_hit_applies_damage_then_invulnerability() {
let mut hp = Health::full(100.0);
assert!(hp.try_hit(30.0, 0.4));
assert_eq!(hp.current, 70.0);
assert!(!hp.try_hit(30.0, 0.4));
assert_eq!(hp.current, 70.0);
}
#[test]
fn health_never_goes_negative() {
let mut hp = Health::full(10.0);
assert!(hp.try_hit(50.0, 0.0));
assert_eq!(hp.current, 0.0);
assert!(hp.is_dead());
assert!(!hp.try_hit(5.0, 0.0));
}
#[test]
fn fraction_handles_zero_max() {
assert_eq!(Health::full(0.0).fraction(), 0.0);
}
}