#![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::*; pub enum AudioMsg { Color([f32; 3]), Fusion, Jump, Switch, } #[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(move_camera) .with_system(character_particle_effect_system), ) .add_system_set( SystemSet::on_update(AppState::Win) .with_system(player_movement_system) .with_system(move_camera) .with_system(character_particle_effect_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, PartialEq)] pub struct CharacterColor(pub Color); #[derive(Component)] pub struct Player; // Systems fn setup( mut commands: Commands, mut current_level: ResMut, mut level_startup_event: EventWriter, mut camera_query: Query<&mut Transform, With>, ) { 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, &mut camera_query, level_id, ); } 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(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); if is_player { entity_commands.insert(Player); audio .send(AudioMsg::Color([color.r(), color.g(), color.b()])) .ok(); } } fn collision_event_system( mut commands: Commands, character_meshes: Res, mut materials: ResMut>, mut collision_events: EventReader, character_query: Query<(&CharacterColor, &Transform, Option<&Player>)>, mut app_state: ResMut>, audio: Res>, ) { for collision_event in collision_events.iter() { if let CollisionEvent::Started(e1, e2, flags) = collision_event { if flags.is_empty() { if let ( Ok((c1_color, c1_transform, c1_player)), Ok((c2_color, c2_transform, 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 && 4. - new_color.length_squared() < 0.1 { 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(); } } } } } 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, With>, mut app_state: ResMut>, 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 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) { audio.send(AudioMsg::Jump).ok(); velocity.linvel.y = 500.; } } if app_state.current() == &AppState::Win && keyboard_input.just_pressed(KeyCode::Return) { app_state.replace(AppState::Game).unwrap(); } } fn character_particle_effect_system( characters: Query<(&Transform, &CharacterColor), With>, mut particle_effect: ResMut, ) { for (transform, color) in characters.iter() { particle_effect.translation = transform.translation; particle_effect.color = color.0; } } fn win_setup(mut commands: Commands, asset_server: Res) { let font = asset_server.get_handle("UacariLegacy-Thin.ttf"); commands .spawn_bundle(Text2dBundle { text: Text::from_section( "Press ENTER to level up", TextStyle { font, font_size: 32.0, color: Color::WHITE, }, ) .with_alignment(TextAlignment::CENTER), ..Default::default() }) .insert(Level); } fn move_camera( mut camera_query: Query<(&Camera, &mut Transform)>, characters: Query<&Transform, (Without, With)>, time: Res