#![allow(clippy::precedence)] #![allow(clippy::too_many_arguments)] use crate::AppState; use bevy::{ input::{keyboard::KeyCode, Input}, prelude::{shape::Quad, *}, sprite::Mesh2dHandle, }; use bevy_fundsp::prelude::*; use bevy_hanabi::*; use bevy_rapier2d::prelude::*; use std::collections::BTreeSet; #[derive(Clone, Copy, Eq, Hash, PartialEq)] pub struct LevelId(pub u32); pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_event::() .init_resource::() .insert_resource(CurrentLevel(None)) .add_system_set(SystemSet::on_enter(AppState::Game).with_system(setup)) .add_system_set(SystemSet::on_enter(AppState::Win).with_system(win_setup)) .add_system_set( SystemSet::on_exit(AppState::Win).with_system(crate::levels::despawn_level), ) .add_system_set( SystemSet::on_update(AppState::Game) .with_system(crate::levels::post_setup_level) .with_system(keyboard_input_system), ) .add_system_set(SystemSet::on_update(AppState::Win).with_system(keyboard_input_system)) .add_system_to_stage(CoreStage::PostUpdate, collision_event_system); } } // Events pub struct LevelStartupEvent(pub Entity); // Resources pub struct CurrentLevel(pub Option); pub struct CharacterMeshes { square: Mesh2dHandle, } impl FromWorld for CharacterMeshes { fn from_world(world: &mut World) -> Self { let mut meshes = world.get_resource_mut::>().unwrap(); Self { square: meshes .add(Mesh::from(Quad { size: Vec2 { x: 64.0, y: 64.0 }, flip: false, })) .into(), } } } // Components #[derive(Component)] pub struct Level; #[derive(Clone, Component, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct CharacterId(pub u32); #[derive(Clone, Component, Copy, Eq, Hash, PartialEq)] pub struct SelectedCharacterId(pub Option); #[derive(Component)] pub struct CharacterIdList(pub BTreeSet); #[derive(Clone, Component, PartialEq)] pub struct CharacterColor(pub Color); // Systems fn setup( mut commands: Commands, mut current_level: ResMut, mut level_startup_event: EventWriter, ) { let level_id = LevelId(current_level.0.map_or(0, |level_id| level_id.0 + 1)); crate::levels::setup_level( &mut commands, &mut current_level, &mut level_startup_event, level_id, ); } pub fn spawn_character( commands: &mut Commands, character_meshes: &Res, effects: &mut ResMut>, materials: &mut ResMut>, selected_character_id: &mut Mut, character_id_list: &mut Mut, transform: Transform, color: Color, ) { let character_id = CharacterId( character_id_list .0 .iter() .last() .map_or(0, |last_character_id| last_character_id.0 + 1), ); character_id_list.0.insert(character_id); let mut gradient = Gradient::new(); gradient.add_key( 0.0, (Vec4::from(color) + Vec4::new(0.1, 0.1, 0.1, 0.0)) .clamp(Vec4::new(0., 0., 0., 0.), Vec4::new(1., 1., 1., 0.)), ); gradient.add_key( 0.2, (Vec4::from(color) + Vec4::new(0.1, 0.1, 0.1, 0.0)) .clamp(Vec4::new(0., 0., 0., 0.), Vec4::new(1., 1., 1., 1.)), ); gradient.add_key( 1.0, (Vec4::from(color) + Vec4::new(0.1, 0.1, 0.1, 0.0)) .clamp(Vec4::new(0., 0., 0., 0.), Vec4::new(1., 1., 1., 0.)), ); commands .spawn_bundle(ColorMesh2dBundle { mesh: character_meshes.square.clone(), material: materials.add(ColorMaterial::from(color)), transform, ..default() }) .insert(Level) .insert(character_id) .insert(CharacterColor(color)) .insert(RigidBody::Dynamic) .insert(Collider::cuboid(32., 32.)) .insert(ExternalForce::default()) .insert(Velocity::default()) .insert(GravityScale(10.0)) .insert(LockedAxes::ROTATION_LOCKED) .insert(Friction::new(0.8)) .insert(Damping { linear_damping: 0.5, angular_damping: 0.5, }) .insert(ExternalImpulse::default()) .insert(ActiveEvents::COLLISION_EVENTS) .with_children(|c| { c.spawn_bundle(ParticleEffectBundle { effect: ParticleEffect::new( effects.add( EffectAsset { name: "Particles".into(), capacity: 4096, spawner: Spawner::rate(30.0.into()) .with_active(selected_character_id.0.is_none()), ..Default::default() } .init(PositionCircleModifier { radius: 30.0, speed: 20.0.into(), dimension: ShapeDimension::Surface, ..Default::default() }) .init(ParticleLifetimeModifier { lifetime: 0.8 }) .render(SizeOverLifetimeModifier { gradient: Gradient::constant(Vec2::splat(4.0)), }) .render(ColorOverLifetimeModifier { gradient }), ), ), transform: Transform::from_xyz(0., 0., 0.1), ..Default::default() }); }); // If no character is selected, then select this one if selected_character_id.0.is_none() { selected_character_id.0 = Some(character_id); } } fn collision_event_system( mut commands: Commands, character_meshes: Res, mut materials: ResMut>, mut effects: ResMut>, mut collision_events: EventReader, character_query: Query<(&CharacterId, &CharacterColor, &Transform)>, mut level_query: Query<(&mut SelectedCharacterId, &mut CharacterIdList)>, mut app_state: ResMut>, ) { for collision_event in collision_events.iter() { if let CollisionEvent::Started(e1, e2, flags) = collision_event { if flags.is_empty() { if let (Ok((c1_id, c1_color, c1_transform)), Ok((c2_id, c2_color, _c2_transform))) = (character_query.get(*e1), character_query.get(*e2)) { let (mut selected_character_id, mut character_id_list) = level_query.single_mut(); character_id_list.0.remove(c1_id); character_id_list.0.remove(c2_id); selected_character_id.0 = None; // TODO completely remove particles commands.entity(*e1).despawn_recursive(); commands.entity(*e2).despawn_recursive(); let new_color = (Vec4::from(c1_color.0) + Vec4::from(c2_color.0)) .clamp(Vec4::ZERO, Vec4::ONE); // If color approximately white if app_state.current() == &AppState::Game && 4. - new_color.length_squared() < 0.1 { app_state.replace(AppState::Win).ok(); } spawn_character( &mut commands, &character_meshes, &mut effects, &mut materials, &mut selected_character_id, &mut character_id_list, *c1_transform, new_color.into(), ); } } } } } fn keyboard_input_system( keyboard_input: Res>, mut characters: Query<( &CharacterId, &mut Velocity, &mut ExternalImpulse, &mut ExternalForce, &Children, )>, mut level_query: Query<(&mut SelectedCharacterId, &CharacterIdList)>, mut effect: Query<&mut ParticleEffect>, dsp_assets: Res, audio: Res