pub use crate::core::danmaku::*;
use crate::app_state::AppState;
use crate::core::collision::TriggerCollider;
use crate::core::danmaku::DanmakuSpawnContext;
use crate::core::danmaku::DanmakuUpdate;
use crate::core::mod_system::BehaviorParams;
use bevy::prelude::*;
use super::BattleUpdate;
#[derive(Resource)]
pub struct BattleInvincibilityConfig {
pub duration: f32,
pub flash_interval: f32,
pub normal_color: Color,
pub flash_color: Color,
}
impl Default for BattleInvincibilityConfig {
fn default() -> Self {
Self {
duration: 1.0,
flash_interval: 0.25,
normal_color: Color::srgb(1.0, 0.0, 0.0), flash_color: Color::srgb(0.5, 0.0, 0.0), }
}
}
#[derive(Resource, Default)]
pub struct BattlePlayerInvincibility {
pub active: bool,
pub timer: f32,
pub flash_timer: f32,
pub flash_state: bool,
}
impl BattlePlayerInvincibility {
pub fn start(&mut self, duration: f32) {
self.active = true;
self.timer = duration;
self.flash_timer = 0.0;
self.flash_state = true;
}
pub fn is_invincible(&self) -> bool {
self.active && self.timer > 0.0
}
}
pub struct DanmakuPlugin;
impl Plugin for DanmakuPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(AppState::Battle), set_battle_context);
app.init_resource::<BattleInvincibilityConfig>()
.init_resource::<BattlePlayerInvincibility>()
.add_systems(
Update,
(
battle_damage_detection_system,
update_battle_invincibility_system,
)
.chain()
.in_set(BattleUpdate),
);
app.register_type::<DanmakuPerformance>()
.register_type::<BulletPrototype>()
.register_type::<BulletVisual>()
.register_type::<ColliderShape>()
.register_type::<BulletBehavior>()
.register_type::<LinearConfig>()
.register_type::<OrbitalConfig>()
.register_type::<SineConfig>()
.register_type::<TweenConfig>()
.register_type::<TweenTarget>()
.register_type::<Easing>()
.register_type::<TimelineEvent>()
.register_type::<SpawnPattern>()
.register_type::<EdgeSide>();
}
}
fn set_battle_context(mut spawn_context: ResMut<DanmakuSpawnContext>) {
*spawn_context = DanmakuSpawnContext::battle();
info!("Danmaku: Set spawn context to Battle");
}
#[allow(clippy::too_many_arguments)]
fn battle_damage_detection_system(
mut commands: Commands,
time: Res<Time>,
invincibility_config: Res<BattleInvincibilityConfig>,
mut player_invincibility: ResMut<BattlePlayerInvincibility>,
mut player_data: ResMut<crate::core::data::PlayerData>,
player_query: Query<(&GlobalTransform, &TriggerCollider), With<BehaviorParams>>,
mut bullet_query: Query<
(
Entity,
&GlobalTransform,
&TriggerCollider,
&crate::core::danmaku::BulletDamage,
&crate::core::danmaku::BulletHitBehavior,
&mut crate::core::danmaku::BulletLastHitTime,
&crate::core::danmaku::BulletMotionState,
),
With<crate::core::danmaku::Bullet>,
>,
mut last_player_state: Local<Option<(Vec2, f64)>>,
#[cfg(all(feature = "bevy_kira_audio", not(feature = "firewheel")))] audio: Res<
bevy_kira_audio::Audio,
>,
asset_server: Res<AssetServer>,
) {
let Ok((player_transform, player_hitbox)) = player_query.single() else {
return;
};
let player_center = player_transform.translation().truncate();
let current_time = time.elapsed_secs_f64();
let player_is_moving = if let Some((last_pos, last_time)) = *last_player_state {
if current_time - last_time > time.delta_secs_f64() * 1.5 {
false
} else {
player_center.distance_squared(last_pos) > 0.0001 }
} else {
false
};
*last_player_state = Some((player_center, current_time));
let is_invincible = player_invincibility.is_invincible();
let bullet_count = bullet_query.iter().count();
if bullet_count > 0 {
static mut LAST_LOG_TIME: f64 = 0.0;
let should_log = unsafe {
if current_time - LAST_LOG_TIME > 1.0 {
LAST_LOG_TIME = current_time;
true
} else {
false
}
};
if should_log {
info!(
"[Battle Damage] Found {} bullets, player at {:?}",
bullet_count, player_center
);
}
}
for (
bullet_entity,
bullet_transform,
bullet_collider,
bullet_damage,
hit_behavior,
mut last_hit_time,
motion_state,
) in bullet_query.iter_mut()
{
let bullet_center = bullet_transform.translation().truncate();
if !check_battle_collision(player_hitbox, player_center, bullet_collider, bullet_center) {
continue;
}
if hit_behavior.invincibility_duration > 0.0 {
let time_since_last_hit = motion_state.elapsed - last_hit_time.0;
if time_since_last_hit < hit_behavior.invincibility_duration {
continue;
}
}
let should_damage = if hit_behavior.damage_on_player_moving {
player_is_moving
} else if hit_behavior.damage_on_player_stationary {
!player_is_moving
} else {
true
};
if is_invincible {
if hit_behavior.despawn_on_hit && should_damage {
commands
.entity(bullet_entity)
.insert(crate::core::danmaku::DespawnBullet);
}
continue;
}
if should_damage {
last_hit_time.0 = motion_state.elapsed;
let damage = bullet_damage.0 as usize;
if player_data.hp > damage {
player_data.hp -= damage;
} else {
player_data.hp = 0;
}
player_invincibility.start(invincibility_config.duration);
#[cfg(all(feature = "bevy_kira_audio", not(feature = "firewheel")))]
crate::core::audio::play_sound(&audio, &asset_server, "hurtsound.wav");
#[cfg(feature = "firewheel")]
crate::core::audio::play_sound(&mut commands, &asset_server, "hurtsound.wav");
info!(
"Battle: Player hit! Damage: {}, HP: {}/{}",
damage, player_data.hp, player_data.hp_max
);
if hit_behavior.despawn_on_hit {
commands
.entity(bullet_entity)
.insert(crate::core::danmaku::DespawnBullet);
}
break;
}
}
}
fn update_battle_invincibility_system(
time: Res<Time>,
invincibility_config: Res<BattleInvincibilityConfig>,
mut player_invincibility: ResMut<BattlePlayerInvincibility>,
mut player_query: Query<&mut Sprite, With<BehaviorParams>>,
) {
if !player_invincibility.active {
return;
}
let delta = time.delta_secs();
player_invincibility.timer -= delta;
if player_invincibility.timer <= 0.0 {
player_invincibility.active = false;
player_invincibility.timer = 0.0;
player_invincibility.flash_state = true;
for mut sprite in player_query.iter_mut() {
sprite.color = invincibility_config.normal_color;
}
info!("Battle: Player invincibility ended");
return;
}
player_invincibility.flash_timer += delta;
if player_invincibility.flash_timer >= invincibility_config.flash_interval {
player_invincibility.flash_timer = 0.0;
player_invincibility.flash_state = !player_invincibility.flash_state;
let color = if player_invincibility.flash_state {
invincibility_config.normal_color
} else {
invincibility_config.flash_color
};
for mut sprite in player_query.iter_mut() {
sprite.color = color;
}
}
}
fn check_battle_collision(
a: &TriggerCollider,
a_center: Vec2,
b: &TriggerCollider,
b_center: Vec2,
) -> bool {
match (a, b) {
(TriggerCollider::Circle { radius: r1 }, TriggerCollider::Circle { radius: r2 }) => {
let dist = a_center.distance(b_center);
dist <= r1 + r2
}
(TriggerCollider::Box { half_size: hs1 }, TriggerCollider::Box { half_size: hs2 }) => {
let diff = (a_center - b_center).abs();
diff.x <= hs1.x + hs2.x && diff.y <= hs1.y + hs2.y
}
(TriggerCollider::Circle { radius }, TriggerCollider::Box { half_size })
| (TriggerCollider::Box { half_size }, TriggerCollider::Circle { radius }) => {
let (circle_center, box_center, box_half) =
if matches!(a, TriggerCollider::Circle { .. }) {
(a_center, b_center, *half_size)
} else {
(b_center, a_center, *half_size)
};
let closest = Vec2::new(
circle_center
.x
.clamp(box_center.x - box_half.x, box_center.x + box_half.x),
circle_center
.y
.clamp(box_center.y - box_half.y, box_center.y + box_half.y),
);
circle_center.distance(closest) <= *radius
}
}
}