#![allow(clippy::precedence)] #![allow(clippy::too_many_arguments)] pub use crate::filters::*; use crate::AppState; use bevy::{ ecs::system::EntityCommands, input::{keyboard::KeyCode, Input}, prelude::{shape::Quad, *}, sprite::Mesh2dHandle, }; use bevy_rapier2d::prelude::*; use rapier2d::geometry::CollisionEventFlags; pub enum AudioMsg { Color([f32; 3]), Fusion, Jump, Switch, } pub struct FirstLevel(pub LevelId); #[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(change_character_system) .with_system(player_movement_system) .with_system(level_keyboard_system) .with_system(move_camera) .with_system(character_particle_effect_system), ) .add_system_set( SystemSet::on_update(AppState::Win) .with_system(player_movement_system) .with_system(level_keyboard_system) .with_system(move_camera) .with_system(character_particle_effect_system) .with_system(move_win_text_system), ) .add_system_to_stage(CoreStage::PostUpdate, collision_event_system); } } // Events pub struct LevelStartupEvent; // 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, PartialEq)] pub struct CharacterColor(pub Color); #[derive(Component)] pub struct Player; #[derive(Component)] pub struct CollisionCount(usize); #[derive(Component)] pub struct Melty(pub Color); #[derive(Component)] pub struct WinText; // Systems fn setup( first_level: Res, mut current_level: ResMut, mut level_startup_event: EventWriter, mut camera_query: Query<&mut Transform, With>, ) { if current_level.0.is_none() { current_level.0 = Some(first_level.0); } crate::levels::setup_level(&mut level_startup_event, &mut camera_query); } pub fn spawn_characters>( commands: &mut Commands, character_meshes: &Res, materials: &mut ResMut>, audio: &Res>, characters: I, ) { for (i, (transform, color)) in characters.into_iter().enumerate() { spawn_character( commands, character_meshes, materials, audio, transform, color, i == 0, ); } } pub fn spawn_character( commands: &mut Commands, character_meshes: &Res, materials: &mut ResMut>, audio: &Res>, mut transform: Transform, color: Color, is_player: bool, ) { transform.translation.z = transform.translation.z.max(1.0); let color_mesh_2d_bundle: ColorMesh2dBundle = ColorMesh2dBundle { mesh: character_meshes.square.clone(), material: materials.add(ColorMaterial::from(color)), transform, ..default() }; let mut entity_commands: EntityCommands = commands.spawn_bundle(color_mesh_2d_bundle); entity_commands .insert(Level) .insert(CharacterColor(color)) .insert(RigidBody::Dynamic) .insert(Collider::cuboid(32., 32.)) .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(ActiveEvents::COLLISION_EVENTS) .with_children(|c| { c.spawn_bundle(TransformBundle::from_transform(Transform::from_xyz( 0., -33., 0., ))) .insert(Sensor) .insert(Collider::cuboid(30., 0.5)) .insert(ActiveEvents::COLLISION_EVENTS) .insert(CollisionCount(0)); }); if is_player { entity_commands.insert(Player); audio .send(AudioMsg::Color([color.r(), color.g(), color.b()])) .ok(); } } pub fn spawn_platforms>( commands: &mut Commands, meshes: &mut ResMut>, materials: &mut ResMut>, platforms: I, ) { for (transform, size) in platforms.into_iter() { spawn_platform(commands, meshes, materials, transform, size); } } pub fn spawn_platform( commands: &mut Commands, meshes: &mut ResMut>, materials: &mut ResMut>, transform: Transform, size: Vec2, ) { commands .spawn_bundle(ColorMesh2dBundle { mesh: meshes.add(Mesh::from(Quad { size, flip: false })).into(), material: materials.add(ColorMaterial::from(Color::GRAY)), transform, ..default() }) .insert(Collider::cuboid(size.x / 2., size.y / 2.)) .insert(Level); } pub fn spawn_melty_platform( commands: &mut Commands, meshes: &mut ResMut>, materials: &mut ResMut>, asset_server: &Res, transform: Transform, color: Color, ) { commands .spawn_bundle(ColorMesh2dBundle { mesh: meshes .add(Mesh::from(Quad { size: Vec2 { x: 96., y: 16. }, flip: false, })) .into(), material: materials.add(ColorMaterial::from(color)), transform, ..default() }) .insert(Collider::cuboid(48., 8.)) .insert(Melty(color)) .insert(Level) .with_children(|c| { c.spawn_bundle(SpriteBundle { texture: asset_server.get_handle("melty.png"), transform: Transform::from_xyz(0., 0., 0.5), ..default() }); }); } fn collision_event_system( mut commands: Commands, character_meshes: Res, mut materials: ResMut>, mut collision_events: EventReader, mut character_query: Query<( &mut CharacterColor, &Transform, &mut Handle, Option<&Player>, )>, pass_through_filter_query: Query<&PassThroughFilter>, melty_query: Query<&Melty>, mut collision_counter_query: Query<&mut CollisionCount>, mut app_state: ResMut>, audio: Res>, ) { for collision_event in collision_events.iter() { match collision_event { CollisionEvent::Started(e1, e2, flags) => { if flags.is_empty() { if let ( Ok((c1_color, c1_transform, _c1_material, c1_player)), Ok((c2_color, c2_transform, _c2_material, c2_player)), ) = (character_query.get(*e1), character_query.get(*e2)) { 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 && new_color.min_element() >= 0.9 { app_state.replace(AppState::Win).ok(); } // position character based on current player location spawn_character( &mut commands, &character_meshes, &mut materials, &audio, if c1_player.is_some() { *c1_transform } else if c2_player.is_some() { *c2_transform } else { Transform::identity().with_translation( (c1_transform.translation + c2_transform.translation) / 2., ) }, new_color.into(), c1_player.is_some() || c2_player.is_some(), ); audio.send(AudioMsg::Fusion).ok(); } else if let (Ok((c_color, _c_transform, _c_material, _c_player)), Ok(melty)) = (character_query.get_mut(*e1), melty_query.get(*e2)) { if (Vec4::from(melty.0) - Vec4::from(c_color.0)).max_element() <= 0. { commands.entity(*e2).despawn_recursive(); } } else if let (Ok((c_color, _c_transform, _c_material, _c_player)), Ok(melty)) = (character_query.get_mut(*e2), melty_query.get(*e1)) { if (Vec4::from(melty.0) - Vec4::from(c_color.0)).max_element() <= 0. { commands.entity(*e1).despawn_recursive(); } } } else if *flags == CollisionEventFlags::SENSOR { if let (Ok((mut c_color, _c_transform, mut c_material, c_player)), Ok(filter)) = ( character_query.get_mut(*e1), pass_through_filter_query.get(*e2), ) { c_color.0 = filter.apply(c_color.0); *c_material = materials.add(ColorMaterial::from(c_color.0)); if c_player.is_some() { audio .send(AudioMsg::Color([ c_color.0.r(), c_color.0.g(), c_color.0.b(), ])) .ok(); audio.send(AudioMsg::Switch).ok(); } } else if let ( Ok((mut c_color, _c_transform, mut c_material, c_player)), Ok(filter), ) = ( character_query.get_mut(*e2), pass_through_filter_query.get(*e1), ) { c_color.0 = filter.apply(c_color.0); *c_material = materials.add(ColorMaterial::from(c_color.0)); if c_player.is_some() { audio .send(AudioMsg::Color([ c_color.0.r(), c_color.0.g(), c_color.0.b(), ])) .ok(); audio.send(AudioMsg::Switch).ok(); } } else if let (Ok(mut collision_count), Err(_)) = ( collision_counter_query.get_mut(*e1), character_query.get_mut(*e2), ) { collision_count.0 += 1; } else if let (Ok(mut collision_count), Err(_)) = ( collision_counter_query.get_mut(*e2), character_query.get_mut(*e1), ) { collision_count.0 += 1; } } } CollisionEvent::Stopped(e1, e2, flags) => { if *flags == CollisionEventFlags::SENSOR { if let (Ok(mut collision_count), Err(_)) = ( collision_counter_query.get_mut(*e1), character_query.get_mut(*e2), ) { collision_count.0 = collision_count.0.saturating_sub(1); } else if let (Ok(mut collision_count), Err(_)) = ( collision_counter_query.get_mut(*e2), character_query.get_mut(*e1), ) { collision_count.0 = collision_count.0.saturating_sub(1); } } } } } } fn change_character_system( mut commands: Commands, keyboard_input: Res>, characters: Query<(Entity, &CharacterColor, Option<&Player>)>, audio: Res>, ) { if !keyboard_input.just_pressed(KeyCode::Tab) { return; } let mut player_idx: usize = 0; let mut player_count: usize = 0; // find player idx for (_entity, _color, player) in characters.iter() { if player.is_some() { player_idx = player_count; } player_count += 1; } // calculate next player index let next_player_idx = (player_idx + 1) % player_count; player_count = 0; // exchange `Player` component from old `player_idx` to new `next_player_idx` for (entity, color, _player) in characters.iter() { if player_count == player_idx { commands.entity(entity).remove::(); } if player_count == next_player_idx { commands.entity(entity).insert(Player); audio .send(AudioMsg::Color([color.0.r(), color.0.g(), color.0.b()])) .ok(); audio.send(AudioMsg::Switch).ok(); } player_count += 1; } } fn player_movement_system( keyboard_input: Res>, mut characters: Query<(&mut Velocity, &Children), With>, collision_counter_query: Query<&CollisionCount>, audio: Res>, ) { let right_pressed: bool = keyboard_input.pressed(KeyCode::Right) || keyboard_input.pressed(KeyCode::D); let left_pressed: bool = keyboard_input.pressed(KeyCode::Left) || keyboard_input.pressed(KeyCode::A); for (mut velocity, children) in characters.iter_mut() { velocity.linvel.x = 200. * (right_pressed as i8 - left_pressed as i8) as f32; if keyboard_input.just_pressed(KeyCode::Space) && collision_counter_query.get(children[0]).unwrap().0 != 0 { audio.send(AudioMsg::Jump).ok(); velocity.linvel.y = 700.; } } } fn character_particle_effect_system( player_character: Query<(&Transform, &CharacterColor), With>, mut particle_effect: ResMut, ) { if let Ok((transform, color)) = player_character.get_single() { particle_effect.translation = transform.translation; particle_effect.color = color.0; } } fn win_setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, asset_server: Res, ) { let font = asset_server.get_handle("UacariLegacy-Thin.ttf"); commands .spawn_bundle(ColorMesh2dBundle { mesh: meshes .add(Mesh::from(Quad { size: Vec2 { x: 512., y: 64. }, flip: false, })) .into(), material: materials.add(ColorMaterial::from(Color::rgba(0., 0., 0., 0.9))), transform: Transform::from_xyz(0., 0., 3.), ..default() }) .insert(Level) .insert(WinText); commands .spawn_bundle(Text2dBundle { text: Text::from_section( "Press ENTER to level up", TextStyle { font, font_size: 36.0, color: Color::WHITE, }, ) .with_alignment(TextAlignment::CENTER), transform: Transform::from_xyz(0., 0., 4.), ..Default::default() }) .insert(Level) .insert(WinText); } fn move_camera( mut camera_query: Query<(&Camera, &mut Transform)>, characters: Query<&Transform, (Without, With)>, time: Res