diff --git a/README.md b/README.md index bcce2c6..b53f02d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ * (?) multiplayer * more audio * "jumpable" component to avoid jumping on sensors -* bug: in level2, move the blue character to win, then reset. The characters are lighter than expected. +* bug: in level2, move the blue character to win, then reset. The characters are lighter than expected. (also level 4) +* redshift warning +* itchio test ## Build diff --git a/assets/game.levels.json b/assets/game.levels.json index d52eafa..bd87468 100644 --- a/assets/game.levels.json +++ b/assets/game.levels.json @@ -147,7 +147,7 @@ "characters": [ { "pos": [ - -128.0, + -160.0, -192.0 ], "color": [ @@ -159,7 +159,7 @@ }, { "pos": [ - 128.0, + 160.0, -192.0 ], "color": [ @@ -219,6 +219,14 @@ 0.0 ], "font_size": 32.0, + "text": "This filter absorbs light." + }, + { + "pos": [ + 0.0, + -64.0 + ], + "font_size": 32.0, "text": "Press R to reset." } ] @@ -297,6 +305,83 @@ } ] }, + { + "comment": "Melting platform tutorial", + "characters": [ + { + "pos": [ + -304.0, + -208.0 + ], + "color": [ + 0.7, + 0.7, + 0.7, + 1.0 + ] + }, + { + "pos": [ + 304.0, + -208.0 + ], + "color": [ + 0.3, + 0.3, + 0.3, + 1.0 + ] + } + ], + "platforms": [ + { + "pos": [ + -304.0, + -256.0 + ], + "size": [ + 192.0, + 16.0 + ] + }, + { + "pos": [ + 304.0, + -256.0 + ], + "size": [ + 192.0, + 16.0 + ] + } + ], + "absorbing_filters": [], + "rotating_filters": [], + "melty_platforms": [ + { + "pos": [ + 0.0, + -256.0 + ], + "color": [ + 0.5, + 0.5, + 0.5, + 1.0 + ] + } + ], + "texts": [ + { + "pos": [ + 0.0, + -64.0 + ], + "font_size": 32.0, + "text": "Too much light\ncause some platforms to melt." + } + ] + }, { "comment": "First puzzle", "characters": [ @@ -564,16 +649,7 @@ ] } ], - "texts": [ - { - "pos": [ - 0.0, - -64.0 - ], - "font_size": 32.0, - "text": "Too much light\ncan cause some platforms to melt." - } - ] + "texts": [] }, { "comment": "Game over", diff --git a/src/game.rs b/src/game.rs index 273e42b..cd92369 100644 --- a/src/game.rs +++ b/src/game.rs @@ -13,6 +13,7 @@ use bevy::{ }; use bevy_rapier2d::prelude::*; use rapier2d::geometry::CollisionEventFlags; +use std::collections::BTreeSet; pub enum AudioMsg { Color([f32; 3]), @@ -33,6 +34,7 @@ impl Plugin for GamePlugin { app.add_event::() .init_resource::() .insert_resource(CurrentLevel(None)) + .init_resource::() .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( @@ -52,7 +54,8 @@ impl Plugin for GamePlugin { .with_system(player_movement_system) .with_system(level_keyboard_system) .with_system(move_camera) - .with_system(character_particle_effect_system), + .with_system(character_particle_effect_system) + .with_system(move_win_text_system), ) .add_system_to_stage(CoreStage::PostUpdate, collision_event_system); } @@ -66,6 +69,9 @@ pub struct LevelStartupEvent; pub struct CurrentLevel(pub Option); +#[derive(Default)] +pub struct CharacterList(pub BTreeSet); + pub struct CharacterMeshes { square: Mesh2dHandle, } @@ -101,6 +107,9 @@ pub struct CollisionCount(usize); #[derive(Component)] pub struct Melty(pub Color); +#[derive(Component)] +pub struct WinText; + // Systems fn setup( @@ -121,6 +130,7 @@ pub fn spawn_characters>( character_meshes: &Res, materials: &mut ResMut>, audio: &Res>, + character_list: &mut ResMut, characters: I, ) { @@ -133,6 +143,7 @@ pub fn spawn_characters>( character_meshes, materials, audio, + character_list, { let mut new_transform: Transform = transform; new_transform.translation.z = curr_z; @@ -150,6 +161,7 @@ pub fn spawn_character( character_meshes: &Res, materials: &mut ResMut>, audio: &Res>, + character_list: &mut ResMut, mut transform: Transform, color: Color, is_player: bool, @@ -189,6 +201,8 @@ pub fn spawn_character( .insert(CollisionCount(0)); }); + character_list.0.insert(entity_commands.id()); + if is_player { entity_commands.insert(Player); audio @@ -277,6 +291,7 @@ fn collision_event_system( mut collision_counter_query: Query<&mut CollisionCount>, mut app_state: ResMut>, audio: Res>, + mut character_list: ResMut, ) { for collision_event in collision_events.iter() { match collision_event { @@ -287,6 +302,8 @@ fn collision_event_system( Ok((c2_color, c2_transform, _c2_material, c2_player)), ) = (character_query.get(*e1), character_query.get(*e2)) { + character_list.0.remove(e1); + character_list.0.remove(e2); commands.entity(*e1).despawn_recursive(); commands.entity(*e2).despawn_recursive(); @@ -294,8 +311,7 @@ fn collision_event_system( .clamp(Vec4::ZERO, Vec4::ONE); // If color approximately white - if app_state.current() == &AppState::Game - && Vec4::from(new_color).min_element() >= 0.9 + if app_state.current() == &AppState::Game && new_color.min_element() >= 0.9 { app_state.replace(AppState::Win).ok(); } @@ -306,6 +322,7 @@ fn collision_event_system( &character_meshes, &mut materials, &audio, + &mut character_list, if c1_player.is_some() { *c1_transform } else if c2_player.is_some() { @@ -409,41 +426,32 @@ fn change_character_system( keyboard_input: Res>, characters: Query<(Entity, &CharacterColor, Option<&Player>)>, audio: Res>, + character_list: 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; + if let Some((player_entity, _color, _)) = characters + .iter() + .find(|(_entity, _color, player)| player.is_some()) + .or_else(|| characters.iter().next()) + { + commands.entity(player_entity).remove::(); + if let Some(new_player_entity) = character_list + .0 + .range(player_entity..) + .nth(1) + .or_else(|| character_list.0.iter().next()) + { + commands.entity(*new_player_entity).insert(Player); + if let Ok((_entity, color, _player)) = characters.get(*new_player_entity) { + audio + .send(AudioMsg::Color([color.0.r(), color.0.g(), color.0.b()])) + .ok(); + audio.send(AudioMsg::Switch).ok(); + } } - 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; } } @@ -499,7 +507,8 @@ fn win_setup( transform: Transform::from_xyz(0., 0., 3.), ..default() }) - .insert(Level); + .insert(Level) + .insert(WinText); commands .spawn_bundle(Text2dBundle { text: Text::from_section( @@ -514,7 +523,8 @@ fn win_setup( transform: Transform::from_xyz(0., 0., 4.), ..Default::default() }) - .insert(Level); + .insert(Level) + .insert(WinText); } fn move_camera( @@ -543,12 +553,24 @@ fn move_camera( } } +fn move_win_text_system( + camera_query: Query<&Transform, With>, + mut win_text_query: Query<&mut Transform, (With, Without)>, +) { + let camera_pos = camera_query.single(); + for mut pos in win_text_query.iter_mut() { + pos.translation.x = camera_pos.translation.x; + pos.translation.y = camera_pos.translation.y; + } +} + fn level_keyboard_system( mut commands: Commands, mut current_level: ResMut, mut level_startup_event: EventWriter, mut camera_query: Query<&mut Transform, With>, keyboard_input: Res>, + mut character_list: ResMut, level_query: Query>, mut app_state: ResMut>, ) { @@ -560,6 +582,7 @@ fn level_keyboard_system( } if keyboard_input.just_pressed(KeyCode::R) { + character_list.0.clear(); for entity in level_query.iter() { commands.entity(entity).despawn_recursive(); } diff --git a/src/levels.rs b/src/levels.rs index 240abdb..9a22e32 100644 --- a/src/levels.rs +++ b/src/levels.rs @@ -16,7 +16,12 @@ pub fn setup_level( level_startup_event.send(LevelStartupEvent); } -pub fn despawn_level(mut commands: Commands, level_query: Query>) { +pub fn despawn_level( + mut commands: Commands, + mut character_list: ResMut, + level_query: Query>, +) { + character_list.0.clear(); for entity in level_query.iter() { commands.entity(entity).despawn_recursive(); } @@ -32,6 +37,7 @@ pub fn post_setup_level( mut level_startup_event: EventReader, asset_server: Res, audio: Res>, + mut character_list: ResMut, stored_levels_assets: Res>, stored_levels_handle: Res>, ) { @@ -50,6 +56,7 @@ pub fn post_setup_level( &mut materials, &asset_server, &audio, + &mut character_list, stored_level, ); } @@ -64,6 +71,7 @@ pub fn spawn_stored_level( materials: &mut ResMut>, asset_server: &Res, audio: &Res>, + character_list: &mut ResMut, stored_level: &StoredLevel, ) { @@ -84,6 +92,7 @@ pub fn spawn_stored_level( character_meshes, materials, audio, + character_list, stored_level.characters.iter().map(|character| { ( Transform::from_xyz(character.pos.x, character.pos.y, 0.), diff --git a/src/menu.rs b/src/menu.rs index fff9778..f35f024 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -20,6 +20,22 @@ impl Plugin for MenuPlugin { fn setup(mut commands: Commands, asset_server: Res) { let font = asset_server.get_handle("UacariLegacy-Thin.ttf"); + #[cfg(target_arch = "wasm32")] + commands + .spawn_bundle(Text2dBundle { + text: Text::from_section( + "Note:\nAudio is NOT available in the WASM build.", + TextStyle { + font: font.clone(), + font_size: 24.0, + color: Color::rgba(1., 0.4, 0.4, 1.), + }, + ) + .with_alignment(TextAlignment::CENTER), + transform: Transform::from_xyz(0., -128.0, 0.), + ..Default::default() + }) + .insert(Menu); commands .spawn_bundle(Text2dBundle { text: Text::from_section( diff --git a/src/particle_effect.rs b/src/particle_effect.rs index a9984b2..4912d46 100644 --- a/src/particle_effect.rs +++ b/src/particle_effect.rs @@ -1,5 +1,5 @@ use bevy::{prelude::*, sprite::Mesh2dHandle}; -use rand::Rng; +use rand::{rngs::ThreadRng, Rng}; use rand_distr::{Distribution, UnitCircle}; #[cfg(not(target_arch = "wasm32"))] @@ -68,15 +68,20 @@ pub struct ParticleComponent { } impl ParticleComponent { - pub fn new() -> Self { + pub fn new(rng: &mut ThreadRng) -> Self { let mut particle_component: Self = Self::default(); - particle_component.randomize_velocity(MIN_VELOCITY, MAX_VELOCITY); + particle_component.randomize_velocity(rng, MIN_VELOCITY, MAX_VELOCITY); particle_component } - pub fn randomize_velocity(&mut self, min_velocity: f32, max_velocity: f32) { - let random_direction: [f32; 2] = UnitCircle.sample(&mut rand::thread_rng()); - let random_magnitude: f32 = rand::thread_rng().gen_range(min_velocity..max_velocity); + pub fn randomize_velocity( + &mut self, + rng: &mut ThreadRng, + min_velocity: f32, + max_velocity: f32, + ) { + let random_direction: [f32; 2] = UnitCircle.sample(rng); + let random_magnitude: f32 = rng.gen_range(min_velocity..max_velocity); self.velocity = Vec3::new(random_direction[0], random_direction[1], 0.0) * random_magnitude; } } @@ -87,6 +92,8 @@ fn particle_effect_startup( particle_mesh: Res, mut materials: ResMut>, ) { + let mut rng = rand::thread_rng(); + for _p in 0..POOL_COUNT { let color_mesh = ColorMesh2dBundle { mesh: particle_mesh.square.clone(), @@ -96,7 +103,7 @@ fn particle_effect_startup( commands .spawn_bundle(color_mesh) - .insert(ParticleComponent::new()); + .insert(ParticleComponent::new(&mut rng)); } } @@ -110,6 +117,8 @@ fn particle_effect_system( mut particle_effect: ResMut, time: Res