diff --git a/assets/particles/star_06.png b/assets/particles/star_06.png new file mode 100644 index 0000000..8ff3b13 Binary files /dev/null and b/assets/particles/star_06.png differ diff --git a/src/battle.rs b/src/battle.rs index fea7a1a..48e976b 100644 --- a/src/battle.rs +++ b/src/battle.rs @@ -7,13 +7,19 @@ use bevy::{ prelude::*, render::{camera::ScalingMode, view::RenderLayers}, }; +use bevy_tweening::{ + lens::TransformPositionLens, Animator, Delay, EaseFunction, Tween, TweeningType, +}; use iyes_loopless::prelude::*; use strum::{EnumCount, IntoEnumIterator}; use strum_macros::{Display, EnumCount, EnumIter, EnumVariantNames}; use crate::{ - board::{BoardPrefab, BoardState, Element, Match}, + board::{ + BoardPrefab, BoardState, Element, Match, Tile, BETWEEN_MATCH_DELAY, MATCH_START_DELAY, + }, cards::{CardsPrefab, CardsState}, + particles::ParticleEmitter, player::{Player, Spell}, prefab::{spawn, Prefab}, transitions::{FadeScreenPrefab, TransitionDirection, TransitionEnd}, @@ -50,6 +56,7 @@ impl Plugin for BattlePlugin { ConditionSet::new() .run_in_state(BattleState::PlayerTurn) .with_system(track_matches) + .with_system(animate_matches) .with_system(start_outtro) .with_system(kill_enemies) .with_system(end_player_turn.run_in_state(BoardState::End)) @@ -57,7 +64,9 @@ impl Plugin for BattlePlugin { ) .add_enter_system( BoardState::End, - player_attack.run_in_state(BattleState::PlayerTurn), + player_attack + .chain(animate_attack) + .run_in_state(BattleState::PlayerTurn), ) .add_enter_system(BattleState::EnemyTurn, enemies_attack) .add_system_set( @@ -301,6 +310,146 @@ fn track_matches(mut events: EventReader<Match>, mut matches: ResMut<Matches>) { matches.0.extend(events.iter().cloned()); } +fn animate_matches( + mut events: EventReader<Match>, + mut commands: Commands, + tiles: Query<&GlobalTransform, With<Tile>>, + mut materials: ResMut<Assets<StandardMaterial>>, + asset_server: Res<AssetServer>, + player: Res<Player>, +) { + if let Some(spell) = player.active_spell.as_ref() { + let start_delay = Duration::from_secs_f32(MATCH_START_DELAY); + let delay_between_matches = Duration::from_secs_f32(BETWEEN_MATCH_DELAY); + + let mut delay = start_delay; + for event in events + .iter() + .filter(|x| spell.elements.contains(&x.element)) + { + let material = materials.add(StandardMaterial { + base_color: event.element.color(), + base_color_texture: Some(asset_server.load("particles/star_06.png")), + double_sided: true, + unlit: true, + alpha_mode: AlphaMode::Blend, + ..default() + }); + + for tile in &event.tiles { + let transform = tiles.get(*tile).unwrap(); + + let transform = transform.compute_transform() * Transform::from_xyz(0.0, 0.0, 1.0); + commands + .spawn_bundle(SpatialBundle { + transform, + ..default() + }) + .insert(BOARD_LAYER) + .insert(DelayedDespawn::from_seconds(0.7)) + .insert(Animator::new(Delay::new(delay).then(Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs_f32(0.5), + TransformPositionLens { + start: transform.translation, + end: Vec3::new(0.0, -2.5, 1.0), + }, + )))) + .with_children(|c| { + c.spawn_bundle(SpatialBundle::default()) + .insert(ParticleEmitter { + material: material.clone(), + timer: Timer::from_seconds(1.0 / 200.0, true), + size_range: 0.2..0.3, + velocity_range: -0.01..0.01, + lifetime_range: 0.5..1.0, + particles_track: true, + }); + + c.spawn_bundle(SpatialBundle::default()) + .insert(ParticleEmitter { + material: material.clone(), + timer: Timer::from_seconds(1.0 / 100.0, true), + size_range: 0.2..0.3, + velocity_range: -0.01..0.01, + lifetime_range: 0.2..0.5, + particles_track: false, + }); + }); + + delay += delay_between_matches; + } + } + } +} + +fn animate_attack( + matches: Res<Matches>, + mut commands: Commands, + mut materials: ResMut<Assets<StandardMaterial>>, + asset_server: Res<AssetServer>, + player: Res<Player>, +) { + if let Some(spell) = player.active_spell.as_ref() { + for event in matches + .0 + .iter() + .filter(|x| spell.elements.contains(&x.element)) + { + let material = materials.add(StandardMaterial { + base_color: event.element.color(), + base_color_texture: Some(asset_server.load("particles/star_06.png")), + double_sided: true, + unlit: true, + alpha_mode: AlphaMode::Blend, + ..default() + }); + + for _ in &event.tiles { + let transform = Transform::from_xyz(0.0, -2.5, 1.0); + commands + .spawn_bundle(SpatialBundle { + transform, + ..default() + }) + .insert(BOARD_LAYER) + .insert(DelayedDespawn::from_seconds(0.7)) + .insert(Animator::new(Tween::new( + EaseFunction::QuadraticInOut, + TweeningType::Once, + Duration::from_secs_f32(0.5), + TransformPositionLens { + start: transform.translation, + end: Vec3::new(0.0, 2.1, 1.0), + }, + ))) + .with_children(|c| { + c.spawn_bundle(SpatialBundle::default()) + .insert(ParticleEmitter { + material: material.clone(), + timer: Timer::from_seconds(1.0 / 200.0, true), + size_range: 0.2..0.3, + velocity_range: -0.01..0.01, + lifetime_range: 0.5..1.0, + particles_track: true, + }); + + c.spawn_bundle(SpatialBundle::default()) + .insert(ParticleEmitter { + material: material.clone(), + timer: Timer::from_seconds(1.0 / 100.0, true), + size_range: 0.2..0.3, + velocity_range: -0.01..0.01, + lifetime_range: 0.2..0.5, + particles_track: false, + }); + }); + } + } + } +} + fn player_attack( mut enemies: Query<(&mut Enemy, &mut EnemyAnimator, &EnemyAnimations)>, mut animation_players: Query<&mut AnimationPlayer>, diff --git a/src/board.rs b/src/board.rs index 0367eec..afedeba 100644 --- a/src/board.rs +++ b/src/board.rs @@ -378,10 +378,13 @@ fn match_gems( events.send_batch(matches.into_iter()); } +pub const MATCH_START_DELAY: f32 = 0.1; +pub const BETWEEN_MATCH_DELAY: f32 = 0.1; + fn destroy_matches(mut events: EventReader<Match>, tiles: Query<&Tile>, mut commands: Commands) { - let start_delay = Duration::from_secs_f32(0.1); + let start_delay = Duration::from_secs_f32(MATCH_START_DELAY); let delay_between_gems = Duration::from_secs_f32(0.0); - let delay_between_matches = Duration::from_secs_f32(0.2); + let delay_between_matches = Duration::from_secs_f32(BETWEEN_MATCH_DELAY); let animation_time = Duration::from_secs_f32(0.1); let mut delay = start_delay; diff --git a/src/lib.rs b/src/lib.rs index 05abef9..46c1030 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,23 +5,25 @@ use board::{BoardPlugin, BoardState}; use cards::{CardPlugin, CardsState}; use iyes_loopless::prelude::*; use main_state::{MainState, MainStatePlugin}; +use particles::ParticlesPlugin; use std::{fmt::Debug, hash::Hash}; use transitions::TransitionPlugin; use utils::UtilsPlugin; +#[cfg(target_arch = "wasm32")] +mod wasm; + mod battle; mod board; mod cards; mod main_state; +mod particles; mod player; mod prefab; mod transitions; mod tween_untils; mod ui; -mod utils; - -#[cfg(target_arch = "wasm32")] -mod wasm; +pub mod utils; pub fn build_app() -> App { let mut app = App::new(); @@ -51,6 +53,7 @@ pub fn build_app() -> App { .add_plugin(BattlePlugin) .add_plugin(TransitionPlugin) .add_plugin(MainStatePlugin) + .add_plugin(ParticlesPlugin) .add_system(log_states::<BoardState>) .add_system(log_states::<BattleState>) .add_system(log_states::<MainState>) diff --git a/src/particles.rs b/src/particles.rs new file mode 100644 index 0000000..404d307 --- /dev/null +++ b/src/particles.rs @@ -0,0 +1,104 @@ +use std::ops::Range; + +use bevy::{ + pbr::{NotShadowCaster, NotShadowReceiver}, + prelude::*, + render::view::RenderLayers, +}; + +use crate::utils::square_mesh; + +pub struct ParticlesPlugin; + +impl Plugin for ParticlesPlugin { + fn build(&self, app: &mut App) { + app.add_system(emit_particles).add_system(move_particles); + } +} + +#[derive(Component)] +pub struct Particle { + pub lifetime: Timer, + pub velocity: Vec3, +} + +#[derive(Component)] +pub struct ParticleEmitter { + pub material: Handle<StandardMaterial>, + pub timer: Timer, + pub size_range: Range<f32>, + pub velocity_range: Range<f32>, + // in seconds + pub lifetime_range: Range<f32>, + pub particles_track: bool, +} + +fn emit_particles( + mut emitters: Query<( + Entity, + &mut ParticleEmitter, + &GlobalTransform, + Option<&RenderLayers>, + )>, + mut commands: Commands, + time: Res<Time>, +) { + let mut rng = fastrand::Rng::new(); + for (entity, mut emitter, transform, render_layers) in &mut emitters { + for _ in 0..(emitter.timer.tick(time.delta()).times_finished_this_tick()) { + let lifetime = random_in_range(&emitter.lifetime_range, &mut rng); + let size = random_in_range(&emitter.size_range, &mut rng); + let velocity = Vec2::new( + random_in_range(&emitter.velocity_range, &mut rng), + random_in_range(&emitter.velocity_range, &mut rng), + ) + .extend(0.0); + + let particle = commands + .spawn_bundle(PbrBundle { + mesh: square_mesh(), + material: emitter.material.clone(), + transform: if emitter.particles_track { + default() + } else { + transform.compute_transform() + } + .with_scale(Vec3::splat(size)), + ..default() + }) + .insert(Particle { + lifetime: Timer::from_seconds(lifetime, false), + velocity, + }) + .insert(NotShadowCaster) + .insert(NotShadowReceiver) + .id(); + + if emitter.particles_track { + commands.entity(entity).add_child(particle); + } + + if let Some(render_layers) = render_layers { + commands.entity(particle).insert(*render_layers); + } + } + } +} + +fn random_in_range(range: &Range<f32>, rng: &mut fastrand::Rng) -> f32 { + rng.f32() * (range.end - range.start) + range.start +} + +fn move_particles( + mut particles: Query<(Entity, &mut Particle, &mut Transform)>, + mut commands: Commands, + time: Res<Time>, +) { + for (entity, mut particle, mut transform) in &mut particles { + if particle.lifetime.tick(time.delta()).finished() { + commands.entity(entity).despawn(); + } else { + transform.translation += particle.velocity; + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 839d21f..c5bf7d9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use std::{hash::Hash, time::Duration}; use bevy::{ asset::HandleId, - ecs::system::AsSystemLabel, + ecs::{query::QueryEntityError, system::AsSystemLabel}, pbr::{NotShadowCaster, NotShadowReceiver}, prelude::{shape::Quad, *}, reflect::TypeUuid, @@ -24,12 +24,20 @@ impl Plugin for UtilsPlugin { .add_event::<WorldCursorEvent>() .add_startup_system(add_meshes) .add_startup_system(add_materials) - .add_system(delayed_despawn) + .add_stage_before( + CoreStage::PostUpdate, + "delayed_despawn", + SystemStage::parallel(), + ) + .add_system_to_stage("delayed_despawn", delayed_despawn) .add_system_to_stage( CoreStage::PostUpdate, update_progress.before(TransformSystem::TransformPropagate), ) - .add_system_to_stage(CoreStage::PostUpdate, propagate_render_layers) + .add_system_to_stage( + "delayed_despawn", + propagate_render_layers.before(delayed_despawn), + ) .add_system_to_stage(CoreStage::PreUpdate, update_world_cursors) .add_system_to_stage( CoreStage::PreUpdate, @@ -153,12 +161,16 @@ fn propagate_render_layers( while !children.is_empty() { for child in std::mem::take(&mut children) { - if let Ok(mut child_layer) = layers.get_mut(child) { - if *child_layer != layer { - *child_layer = layer; + match layers.get_mut(child) { + Ok(mut child_layer) => { + if *child_layer != layer { + *child_layer = layer; + } } - } else { - commands.entity(child).insert(layer); + Err(QueryEntityError::QueryDoesNotMatch(entity)) => { + commands.entity(entity).insert(layer); + } + _ => {} } children.extend(children_query.get(child).into_iter().flatten().cloned());