Skip to content

Commit

Permalink
feat: Add earth prison spell to demon boss
Browse files Browse the repository at this point in the history
  • Loading branch information
PraxTube committed Dec 20, 2023
1 parent 95a2988 commit 927f1cd
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 9 deletions.
Binary file added assets/sounds/demon_boss_vocal_earth_prison.ogg
Binary file not shown.
2 changes: 2 additions & 0 deletions src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ pub struct GameAssets {
pub demon_boss_step_sound: Handle<AudioSource>,
#[asset(path = "sounds/demon_boss_vocal_explosion.ogg")]
pub demon_boss_vocal_explosion_sound: Handle<AudioSource>,
#[asset(path = "sounds/demon_boss_vocal_earth_prison.ogg")]
pub demon_boss_vocal_earth_prison_sound: Handle<AudioSource>,
#[asset(path = "sounds/gitgud.ogg")]
pub git_gud: Handle<AudioSource>,

Expand Down
1 change: 1 addition & 0 deletions src/enemy/demon_boss/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ fn play_cast_vocals(
for demon_spell in &q_demon_spells {
let vocals = match demon_spell.spell {
DemonSpell::Explosion => assets.demon_boss_vocal_explosion_sound.clone(),
DemonSpell::EarthPrison => assets.demon_boss_vocal_earth_prison_sound.clone(),
};

play_sound.send(PlaySound {
Expand Down
56 changes: 49 additions & 7 deletions src/enemy/demon_boss/cast.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use rand::{thread_rng, Rng};

use bevy::prelude::*;

use super::{state::DemonBossState, DemonBoss};
use crate::GameState;

#[derive(Clone, PartialEq)]
#[derive(Clone, PartialEq, Copy)]
pub enum DemonSpell {
Explosion,
EarthPrison,
}

#[derive(Component, Clone)]
Expand All @@ -14,16 +17,28 @@ pub struct DemonSpellCast {
pub timer: Timer,
}

#[derive(Event, Deref, DerefMut)]
pub struct SpawnDemonSpell(pub DemonSpellCast);

#[derive(Component)]
pub struct DemonSpellCooldown {
#[allow(dead_code)]
spell: DemonSpell,
timer: Timer,
}

#[derive(Component, Deref, DerefMut)]
pub struct LastSpellTimer(Timer);

#[derive(Event, Deref, DerefMut)]
pub struct SpawnDemonSpell(pub DemonSpellCast);

fn pick_random_spell() -> DemonSpell {
let mut rng = thread_rng();

match rng.gen_range(0..2) {
0 => DemonSpell::Explosion,
_ => DemonSpell::EarthPrison,
}
}

fn spawn_demon_spell(
mut commands: Commands,
q_demon_boss: Query<&DemonBoss>,
Expand All @@ -45,12 +60,14 @@ fn spawn_demon_spell(
return;
}

let spell = pick_random_spell();

commands.spawn(DemonSpellCooldown {
spell: DemonSpell::Explosion,
spell,
timer: Timer::from_seconds(5.0, TimerMode::Once),
});
commands.spawn(DemonSpellCast {
spell: DemonSpell::Explosion,
spell,
timer: Timer::from_seconds(1.5, TimerMode::Once),
});
}
Expand Down Expand Up @@ -83,6 +100,29 @@ fn despawn_spell_cooldowns(
}
}

fn spawn_last_spell_timer(mut commands: Commands) {
commands.spawn(LastSpellTimer(Timer::from_seconds(10.0, TimerMode::Once)));
}

fn reset_last_spell_timer(
time: Res<Time>,
mut q_last_spell_timer: Query<&mut LastSpellTimer>,
mut ev_spawn_demon_spell: EventReader<SpawnDemonSpell>,
) {
let mut timer = match q_last_spell_timer.get_single_mut() {
Ok(r) => r,
Err(_) => return,
};
timer.tick(time.delta());

if ev_spawn_demon_spell.is_empty() {
return;
}
ev_spawn_demon_spell.clear();

timer.reset();
}

pub struct DemonBossCastPlugin;

impl Plugin for DemonBossCastPlugin {
Expand All @@ -93,9 +133,11 @@ impl Plugin for DemonBossCastPlugin {
spawn_demon_spell,
despawn_spell_cooldowns,
relay_demon_spells,
reset_last_spell_timer,
)
.run_if(in_state(GameState::Gaming)),
)
.add_event::<SpawnDemonSpell>();
.add_event::<SpawnDemonSpell>()
.add_systems(OnEnter(GameState::Gaming), spawn_last_spell_timer);
}
}
163 changes: 163 additions & 0 deletions src/enemy/demon_boss/earth_prison.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use std::f32::consts::{PI, TAU};

use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use bevy_trickfilm::prelude::*;

use crate::{audio::PlaySound, player::Player, world::camera::YSort, GameAssets, GameState};

use super::{
cast::{DemonSpell, SpawnDemonSpell},
DemonBoss,
};

const WALL_PADDING: f32 = 10.0;
const COUNT: usize = 25;
const RADIUS: f32 = 100.0;
const OFFSET: f32 = 50.0;

#[derive(Component, Default)]
struct DemonBossEarthWall {
despawning: bool,
}
#[derive(Component)]
struct DemonBossEarthWallCollider {
timer: Timer,
}

fn spawn_wall(commands: &mut Commands, assets: &Res<GameAssets>, pos: Vec3, flip_x: bool) {
let mut animator = AnimationPlayer2D::default();
animator.play(assets.earth_wall_animations[0].clone());

commands.spawn((
DemonBossEarthWall::default(),
animator,
YSort(0.0),
SpriteSheetBundle {
transform: Transform::from_translation(pos),
texture_atlas: assets.earth_wall.clone(),
sprite: TextureAtlasSprite {
flip_x,
..default()
},
..default()
},
));
}

fn spawn_collider(commands: &mut Commands, pos: Vec3, offset: f32, angle: f32) {
let mut vertices = Vec::new();
for i in 0..COUNT {
let rot = Quat::from_rotation_z(angle + 3.0 / 2.0 * PI * i as f32 / COUNT as f32);
let pos = rot.mul_vec3(Vec3::X * offset - WALL_PADDING);
vertices.push(Vect::new(pos.x, pos.y));
}

commands.spawn((
DemonBossEarthWallCollider {
timer: Timer::from_seconds(7.0, TimerMode::Once),
},
Collider::polyline(vertices, None),
CollisionGroups::default(),
TransformBundle::from_transform(Transform::from_translation(pos)),
));
}

fn spawn_earth_prison(
mut commands: Commands,
assets: Res<GameAssets>,
q_player: Query<&Transform, With<Player>>,
q_demon_boss: Query<&Transform, (With<DemonBoss>, Without<Player>)>,
mut ev_spawn_demon_spells: EventReader<SpawnDemonSpell>,
mut ev_play_sound: EventWriter<PlaySound>,
) {
let player_pos = match q_player.get_single() {
Ok(r) => r.translation,
Err(_) => return,
};
let demon_boss_pos = match q_demon_boss.get_single() {
Ok(r) => r.translation,
Err(_) => return,
};

let dis = demon_boss_pos - player_pos;
let pos = player_pos + dis.normalize_or_zero() * OFFSET;
let angle = dis.angle_between(Vec3::X) + PI / 4.0;

for ev in ev_spawn_demon_spells.read() {
if ev.spell != DemonSpell::EarthPrison {
continue;
}

ev_play_sound.send(PlaySound {
clip: assets.earth_wall_sound.clone(),
..default()
});

spawn_collider(&mut commands, pos, RADIUS, angle);
for i in 0..COUNT {
let rot = Quat::from_rotation_z(angle + 3.0 / 2.0 * PI * i as f32 / COUNT as f32);
let pos = pos + rot.mul_vec3(Vec3::X * RADIUS);
let flip_x = rot.to_euler(EulerRot::ZYX).0.abs() < TAU / 4.0;
spawn_wall(&mut commands, &assets, pos, flip_x);
}
}
}

fn despawn_earth_prison(
mut commands: Commands,
assets: Res<GameAssets>,
time: Res<Time>,
mut q_earth_walls: Query<(&mut DemonBossEarthWall, &mut AnimationPlayer2D)>,
mut q_earth_wall_colliders: Query<(Entity, &mut DemonBossEarthWallCollider)>,
) {
let mut despawn = false;
for (_, mut collider) in &mut q_earth_wall_colliders {
collider.timer.tick(time.delta());
if collider.timer.just_finished() {
despawn = true;
}
}

if !despawn {
return;
}

for (entity, _) in &q_earth_wall_colliders {
commands.entity(entity).despawn_recursive();
}

for (mut earth_wall, mut animator) in &mut q_earth_walls {
if !earth_wall.despawning {
earth_wall.despawning = true;
animator.play(assets.earth_wall_animations[1].clone());
}
}
}

fn despawn_walls(
mut commands: Commands,
q_earth_walls: Query<(Entity, &DemonBossEarthWall, &AnimationPlayer2D)>,
) {
for (entity, earth_wall, animator) in &q_earth_walls {
if !earth_wall.despawning {
continue;
}

if animator.is_finished() {
commands.entity(entity).despawn_recursive();
}
}
}

pub struct DemonBossEarthPrisonPlugin;

impl Plugin for DemonBossEarthPrisonPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(spawn_earth_prison, despawn_earth_prison, despawn_walls)
.run_if(in_state(GameState::Gaming)),
);
}
}
2 changes: 2 additions & 0 deletions src/enemy/demon_boss/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod explosion;
mod audio;
mod cast;
mod collision;
mod earth_prison;
mod movement;
mod rage;
mod spawn;
Expand All @@ -27,6 +28,7 @@ impl Plugin for DemonBossPlugin {
explosion::DemonBossExplosionPlugin,
collision::DemonBossCollisionPlugin,
rage::DemonBossRagePlugin,
earth_prison::DemonBossEarthPrisonPlugin,
));
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/enemy/demon_boss/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bevy_trickfilm::prelude::*;
use crate::{player::Player, GameAssets, GameState};

use super::{
cast::{DemonSpellCooldown, SpawnDemonSpell},
cast::{DemonSpellCooldown, LastSpellTimer, SpawnDemonSpell},
movement::MovementCooldownTimer,
strike::StrikeCooldown,
DemonBoss, INV_CAST_RANGE, STRIKE_RANGE,
Expand Down Expand Up @@ -163,6 +163,7 @@ fn switch_to_casting(
mut q_demon_boss: Query<(&Transform, &mut DemonBoss)>,
q_player: Query<&Transform, (With<Player>, Without<DemonBoss>)>,
q_demon_spell_cooldown: Query<&DemonSpellCooldown>,
q_last_spell_timer: Query<&LastSpellTimer>,
) {
if !q_demon_spell_cooldown.is_empty() {
return;
Expand All @@ -175,6 +176,10 @@ fn switch_to_casting(
Ok(p) => p.translation,
Err(_) => return,
};
let last_spell_timer = match q_last_spell_timer.get_single() {
Ok(r) => r,
Err(_) => return,
};

if demon_boss.state == DemonBossState::Casting {
return;
Expand All @@ -188,7 +193,7 @@ fn switch_to_casting(
.distance_squared(demon_boss_transform.translation.truncate());
let inv_cast_range = INV_CAST_RANGE.powi(2);

if dis >= inv_cast_range {
if dis >= inv_cast_range || last_spell_timer.finished() {
demon_boss.state = DemonBossState::Casting;
}
}
Expand Down

0 comments on commit 927f1cd

Please sign in to comment.