#![allow(clippy::precedence)] #![allow(clippy::too_many_arguments)] pub use crate::audio::AudioMsg; use crate::AppState; use bevy::{ input::{keyboard::KeyCode, Input}, prelude::{shape::Quad, *}, sprite::Mesh2dHandle, }; 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) .with_system(character_particle_effect_system) .with_system(move_camera), ) .add_system_set( SystemSet::on_update(AppState::Win) .with_system(keyboard_input_system) .with_system(move_camera), ) .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, 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_character( commands: &mut Commands, character_meshes: &Res, materials: &mut ResMut>, selected_character_id: &mut Mut, character_id_list: &mut Mut, audio: &Res>, 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); 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); // If no character is selected, then select this one if selected_character_id.0.is_none() { selected_character_id.0 = Some(character_id); 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<(&CharacterId, &CharacterColor, &Transform)>, mut level_query: Query<(&mut SelectedCharacterId, &mut CharacterIdList)>, 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_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 materials, &mut selected_character_id, &mut character_id_list, &audio, *c1_transform, new_color.into(), ); } } } } } fn keyboard_input_system( keyboard_input: Res>, mut characters: Query<(&CharacterId, &mut Velocity, &CharacterColor)>, mut level_query: Query<(&mut SelectedCharacterId, &CharacterIdList)>, mut app_state: ResMut>, audio: Res>, ) { if let Ok((mut selected_character_id, character_id_list)) = level_query.get_single_mut() { if keyboard_input.just_pressed(KeyCode::Tab) { let selected = if let Some(selected_character_id) = &mut selected_character_id.0 { *selected_character_id = *character_id_list .0 .range(*selected_character_id..) .nth(1) .unwrap_or_else(|| character_id_list.0.iter().next().unwrap()); *selected_character_id } else { selected_character_id.0 = Some(CharacterId(0)); CharacterId(0) }; if let Some((_character_id, _velocity, color)) = characters .iter_mut() .find(|(character_id, _velocity, _color)| **character_id == selected) { audio .send(AudioMsg::Color([color.0.r(), color.0.g(), color.0.b()])) .ok(); } } 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); if let Some(selected_character_id) = &selected_character_id.0 { if let Some((_character_id, mut velocity, _color)) = characters .iter_mut() .find(|(character_id, _velocity, _color)| { *character_id == selected_character_id }) { 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( mut characters: Query<(&CharacterId, &Transform, &CharacterColor)>, mut particle_effect: ResMut, mut level_query: Query<(&SelectedCharacterId)>, ) { if let Ok(selected_character_id) = level_query.get_single_mut() { if let Some(selected_character_id) = &selected_character_id.0 { if let Some((_character_id, transform, color)) = characters .iter_mut() .find(|(character_id, _transform, _color)| { *character_id == selected_character_id }) { 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, &GlobalTransform)>, characters: Query<(&CharacterId, &Transform), Without>, level_query: Query<&SelectedCharacterId>, ) { const MARGIN: f32 = 128.0; if let Ok(selected_character_id) = level_query.get_single() { if let Some(selected_character_id) = &selected_character_id.0 { if let Some((_character_id, transform)) = characters .iter() .find(|(character_id, _transform)| *character_id == selected_character_id) { let (camera, mut camera_transform, camera_global_transform) = camera_query.single_mut(); let pos = camera .world_to_viewport(camera_global_transform, transform.translation) .unwrap(); let size = camera.logical_viewport_size().unwrap(); if pos.x < MARGIN { camera_transform.translation.x += pos.x - MARGIN; } if pos.x > size.x - MARGIN { camera_transform.translation.x += MARGIN + pos.x - size.x; } if pos.y < MARGIN { camera_transform.translation.y += pos.y - MARGIN; } if pos.y > size.y - MARGIN { camera_transform.translation.y += MARGIN + pos.y - size.y; } } } } }