diff --git a/README.md b/README.md
index 24a0de6..58a330a 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,22 @@
# Bevyjam
+## Controls
+
+* **Move**: arrows
+* **Switch character**: Tab
+* **Level up**: Enter (when character is white)
+
## TODO
* name
-* scene management
* stream audio (with HexoSynthDSP)
* color filters
* level design
-* win
-* menu GUI
* (?) can jump only from a surface (no mid-air jump)
* (?) multiplayer
* make WASM build work again (replace hanabi)
+* Text is not displayed after menu (on win state and on game over level)
+* level reset
## Build
diff --git a/src/game.rs b/src/game.rs
index b8682a8..bdd90aa 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -13,6 +13,9 @@ 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 {
@@ -21,24 +24,29 @@ impl Plugin for GamePlugin {
.init_resource::<CharacterMeshes>()
.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(post_setup_level)
+ .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
-struct LevelStartupEvent(Entity);
+pub struct LevelStartupEvent(pub Entity);
// Resources
-struct CurrentLevel(Option<Entity>);
+pub struct CurrentLevel(pub Option<LevelId>);
-struct CharacterMeshes {
+pub struct CharacterMeshes {
square: Mesh2dHandle,
}
@@ -58,20 +66,20 @@ impl FromWorld for CharacterMeshes {
// Components
-#[derive(Clone, Component, Copy, Eq, Hash, PartialEq)]
-struct LevelId(u32);
+#[derive(Component)]
+pub struct Level;
#[derive(Clone, Component, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
-struct CharacterId(u32);
+pub struct CharacterId(pub u32);
#[derive(Clone, Component, Copy, Eq, Hash, PartialEq)]
-struct SelectedCharacterId(Option<CharacterId>);
+pub struct SelectedCharacterId(pub Option<CharacterId>);
#[derive(Component)]
-struct CharacterIdList(BTreeSet<CharacterId>);
+pub struct CharacterIdList(pub BTreeSet<CharacterId>);
#[derive(Clone, Component, PartialEq)]
-struct CharacterColor(Color);
+pub struct CharacterColor(pub Color);
// Systems
@@ -80,69 +88,16 @@ fn setup(
mut current_level: ResMut<CurrentLevel>,
mut level_startup_event: EventWriter<LevelStartupEvent>,
) {
- let level_entity = commands
- .spawn()
- .insert(LevelId(0))
- .insert(SelectedCharacterId(None))
- .insert(CharacterIdList(BTreeSet::new()))
- .id();
- current_level.0 = Some(level_entity);
-
- commands
- .spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, -256.0, 0.0)))
- .insert(Collider::cuboid(400., 10.));
-
- level_startup_event.send(LevelStartupEvent(level_entity));
+ 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,
+ );
}
-// This is a bad design, but it's the only way I found
-fn post_setup_level(
- mut commands: Commands,
- character_meshes: Res<CharacterMeshes>,
- mut effects: ResMut<Assets<EffectAsset>>,
- mut materials: ResMut<Assets<ColorMaterial>>,
- mut level_query: Query<(&mut SelectedCharacterId, &mut CharacterIdList)>,
- mut level_startup_event: EventReader<LevelStartupEvent>,
-) {
- for LevelStartupEvent(level_entity) in level_startup_event.iter() {
- if let Ok((mut selected_character_id, mut character_id_list)) =
- level_query.get_mut(*level_entity)
- {
- spawn_character(
- &mut commands,
- &character_meshes,
- &mut effects,
- &mut materials,
- &mut selected_character_id,
- &mut character_id_list,
- Transform::from_xyz(-128., -64., 0.),
- Color::RED,
- );
- spawn_character(
- &mut commands,
- &character_meshes,
- &mut effects,
- &mut materials,
- &mut selected_character_id,
- &mut character_id_list,
- Transform::from_xyz(0., -64., 0.),
- Color::GREEN,
- );
- spawn_character(
- &mut commands,
- &character_meshes,
- &mut effects,
- &mut materials,
- &mut selected_character_id,
- &mut character_id_list,
- Transform::from_xyz(128., -64., 0.),
- Color::BLUE,
- );
- }
- }
-}
-
-fn spawn_character(
+pub fn spawn_character(
commands: &mut Commands,
character_meshes: &Res<CharacterMeshes>,
effects: &mut ResMut<Assets<EffectAsset>>,
@@ -184,6 +139,7 @@ fn spawn_character(
transform,
..default()
})
+ .insert(Level)
.insert(character_id)
.insert(CharacterColor(color))
.insert(RigidBody::Dynamic)
@@ -239,41 +195,48 @@ fn collision_event_system(
character_meshes: Res<CharacterMeshes>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut effects: ResMut<Assets<EffectAsset>>,
- current_level: Res<CurrentLevel>,
mut collision_events: EventReader<CollisionEvent>,
character_query: Query<(&CharacterId, &CharacterColor, &Transform)>,
mut level_query: Query<(&mut SelectedCharacterId, &mut CharacterIdList)>,
+ mut app_state: ResMut<State<AppState>>,
) {
- if let Some(level_entity) = current_level.0 {
- 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))
+ 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
{
- let (mut selected_character_id, mut character_id_list) =
- level_query.get_mut(level_entity).unwrap();
- 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();
-
- spawn_character(
- &mut commands,
- &character_meshes,
- &mut effects,
- &mut materials,
- &mut selected_character_id,
- &mut character_id_list,
- *c1_transform,
- (Vec4::from(c1_color.0) + Vec4::from(c2_color.0)).into(),
- );
+ println!("win");
+ 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(),
+ );
}
}
}
@@ -282,7 +245,6 @@ fn collision_event_system(
fn keyboard_input_system(
keyboard_input: Res<Input<KeyCode>>,
- current_level: Res<CurrentLevel>,
mut characters: Query<(
&CharacterId,
&mut Velocity,
@@ -294,73 +256,92 @@ fn keyboard_input_system(
mut effect: Query<&mut ParticleEffect>,
dsp_assets: Res<DspAssets>,
audio: Res<Audio>,
+ mut app_state: ResMut<State<AppState>>,
) {
- if let Some(level_entity) = current_level.0 {
- if let Ok((mut selected_character_id, character_id_list)) =
- level_query.get_mut(level_entity)
- {
- if keyboard_input.just_pressed(KeyCode::Tab) {
- audio.play(dsp_assets.graph(&sine_wave));
-
- let selected = if let Some(selected_character_id) = &mut selected_character_id.0 {
- if let Some((_character_id, _velocity, _impulse, _force, children)) = characters
- .iter_mut()
- .find(|(character_id, _velocity, _impulse, _force, _children)| {
- *character_id == selected_character_id
- }) {
- effect
- .get_mut(children[0])
- .unwrap()
- .maybe_spawner()
- .unwrap()
- .set_active(false);
- }
-
- *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 Ok((mut selected_character_id, character_id_list)) = level_query.get_single_mut() {
+ if keyboard_input.just_pressed(KeyCode::Tab) {
+ audio.play(dsp_assets.graph(&sine_wave));
+ let selected = if let Some(selected_character_id) = &mut selected_character_id.0 {
if let Some((_character_id, _velocity, _impulse, _force, children)) = characters
.iter_mut()
.find(|(character_id, _velocity, _impulse, _force, _children)| {
- **character_id == selected
+ *character_id == selected_character_id
}) {
effect
.get_mut(children[0])
.unwrap()
.maybe_spawner()
.unwrap()
- .set_active(true);
+ .set_active(false);
}
+
+ *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, _impulse, _force, children)) = characters
+ .iter_mut()
+ .find(|(character_id, _velocity, _impulse, _force, _children)| {
+ **character_id == selected
+ }) {
+ effect
+ .get_mut(children[0])
+ .unwrap()
+ .maybe_spawner()
+ .unwrap()
+ .set_active(true);
}
+ }
- 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);
+ 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, _impulse, _force, _children)) = characters
- .iter_mut()
- .find(|(character_id, _velocity, _impulse, _force, _children)| {
- *character_id == selected_character_id
- }) {
- velocity.linvel.x = 200. * (right_pressed as i8 - left_pressed as i8) as f32;
+ if let Some(selected_character_id) = &selected_character_id.0 {
+ if let Some((_character_id, mut velocity, _impulse, _force, _children)) = characters
+ .iter_mut()
+ .find(|(character_id, _velocity, _impulse, _force, _children)| {
+ *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) {
- velocity.linvel.y = 500.;
- }
+ if keyboard_input.just_pressed(KeyCode::Space) {
+ velocity.linvel.y = 500.;
}
}
}
}
+
+ if app_state.current() == &AppState::Win && keyboard_input.just_pressed(KeyCode::Return) {
+ app_state.replace(AppState::Game).unwrap();
+ }
+}
+
+// TODO: the text is not visible, I don't know why.
+fn win_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
+ println!("win system");
+ let font = asset_server.get_handle("Cantarell-VF.otf");
+ 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()
+ });
}
// Sounds
diff --git a/src/levels.rs b/src/levels.rs
new file mode 100644
index 0000000..4bb361f
--- /dev/null
+++ b/src/levels.rs
@@ -0,0 +1,74 @@
+#![allow(clippy::too_many_arguments)]
+
+mod game_over;
+mod level0;
+mod level1;
+
+use crate::game::*;
+
+use bevy::prelude::*;
+use bevy_hanabi::*;
+use std::collections::BTreeSet;
+
+pub fn setup_level(
+ commands: &mut Commands,
+ current_level: &mut ResMut<CurrentLevel>,
+ level_startup_event: &mut EventWriter<LevelStartupEvent>,
+ level_id: LevelId,
+) {
+ let level_entity = commands
+ .spawn()
+ .insert(Level)
+ .insert(SelectedCharacterId(None))
+ .insert(CharacterIdList(BTreeSet::new()))
+ .id();
+ current_level.0 = Some(level_id);
+
+ level_startup_event.send(LevelStartupEvent(level_entity));
+}
+
+pub fn despawn_level(mut commands: Commands, level_query: Query<Entity, With<Level>>) {
+ for entity in level_query.iter() {
+ commands.entity(entity).despawn_recursive();
+ }
+}
+
+// This is a bad design, to be refactored some days
+pub fn post_setup_level(
+ mut commands: Commands,
+ character_meshes: Res<CharacterMeshes>,
+ mut effects: ResMut<Assets<EffectAsset>>,
+ mut materials: ResMut<Assets<ColorMaterial>>,
+ current_level: Res<CurrentLevel>,
+ mut level_query: Query<(&mut SelectedCharacterId, &mut CharacterIdList)>,
+ mut level_startup_event: EventReader<LevelStartupEvent>,
+ asset_server: Res<AssetServer>,
+) {
+ for LevelStartupEvent(level_entity) in level_startup_event.iter() {
+ if let Ok((mut selected_character_id, mut character_id_list)) =
+ level_query.get_mut(*level_entity)
+ {
+ if let Some(level_id) = current_level.0 {
+ match level_id.0 {
+ 0 => level0::setup(
+ &mut commands,
+ &character_meshes,
+ &mut effects,
+ &mut materials,
+ &mut selected_character_id,
+ &mut character_id_list,
+ ),
+ 1 => level1::setup(
+ &mut commands,
+ &character_meshes,
+ &mut effects,
+ &mut materials,
+ &mut selected_character_id,
+ &mut character_id_list,
+ ),
+ _ => game_over::setup(&mut commands, &asset_server),
+ }
+ }
+ }
+ }
+}
diff --git a/src/levels/game_over.rs b/src/levels/game_over.rs
new file mode 100644
index 0000000..d46c2bf
--- /dev/null
+++ b/src/levels/game_over.rs
@@ -0,0 +1,36 @@
+use crate::game::*;
+
+use bevy::prelude::*;
+
+pub fn setup(commands: &mut Commands, asset_server: &Res<AssetServer>) {
+ let font = asset_server.get_handle("Cantarell-VF.otf");
+ commands
+ .spawn_bundle(Text2dBundle {
+ text: Text::from_section(
+ "GAME OVER",
+ TextStyle {
+ font: font.clone(),
+ font_size: 48.0,
+ color: Color::WHITE,
+ },
+ )
+ .with_alignment(TextAlignment::CENTER),
+ transform: Transform::from_xyz(0., -128.0, 0.),
+ ..Default::default()
+ })
+ .insert(Level);
+ commands
+ .spawn_bundle(Text2dBundle {
+ text: Text::from_section(
+ "There is no more light to combine.",
+ TextStyle {
+ font,
+ font_size: 32.0,
+ color: Color::WHITE,
+ },
+ )
+ .with_alignment(TextAlignment::CENTER),
+ ..Default::default()
+ })
+ .insert(Level);
+}
diff --git a/src/levels/level0.rs b/src/levels/level0.rs
new file mode 100644
index 0000000..2bde3dd
--- /dev/null
+++ b/src/levels/level0.rs
@@ -0,0 +1,50 @@
+use crate::game::*;
+
+use bevy::prelude::*;
+use bevy_hanabi::*;
+use bevy_rapier2d::prelude::*;
+
+pub fn setup(
+ commands: &mut Commands,
+ character_meshes: &Res<CharacterMeshes>,
+ effects: &mut ResMut<Assets<EffectAsset>>,
+ materials: &mut ResMut<Assets<ColorMaterial>>,
+ selected_character_id: &mut Mut<SelectedCharacterId>,
+ character_id_list: &mut Mut<CharacterIdList>,
+) {
+ commands
+ .spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, -256.0, 0.0)))
+ .insert(Collider::cuboid(400., 10.))
+ .insert(Level);
+
+ spawn_character(
+ commands,
+ character_meshes,
+ effects,
+ materials,
+ selected_character_id,
+ character_id_list,
+ Transform::from_xyz(-128., -64., 0.),
+ Color::RED,
+ );
+ spawn_character(
+ commands,
+ character_meshes,
+ effects,
+ materials,
+ selected_character_id,
+ character_id_list,
+ Transform::from_xyz(0., -64., 0.),
+ Color::GREEN,
+ );
+ spawn_character(
+ commands,
+ character_meshes,
+ effects,
+ materials,
+ selected_character_id,
+ character_id_list,
+ Transform::from_xyz(128., -64., 0.),
+ Color::BLUE,
+ );
+}
diff --git a/src/levels/level1.rs b/src/levels/level1.rs
new file mode 100644
index 0000000..06325b7
--- /dev/null
+++ b/src/levels/level1.rs
@@ -0,0 +1,56 @@
+use crate::game::*;
+
+use bevy::prelude::*;
+use bevy_hanabi::*;
+use bevy_rapier2d::prelude::*;
+
+pub fn setup(
+ commands: &mut Commands,
+ character_meshes: &Res<CharacterMeshes>,
+ effects: &mut ResMut<Assets<EffectAsset>>,
+ materials: &mut ResMut<Assets<ColorMaterial>>,
+ selected_character_id: &mut Mut<SelectedCharacterId>,
+ character_id_list: &mut Mut<CharacterIdList>,
+) {
+ commands
+ .spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, -256.0, 0.0)))
+ .insert(Collider::cuboid(400., 10.))
+ .insert(Level);
+ commands
+ .spawn_bundle(TransformBundle::from(Transform::from_xyz(
+ 256.0, -128.0, 0.0,
+ )))
+ .insert(Collider::cuboid(200., 10.))
+ .insert(Level);
+
+ spawn_character(
+ commands,
+ character_meshes,
+ effects,
+ materials,
+ selected_character_id,
+ character_id_list,
+ Transform::from_xyz(128., 64., 0.),
+ Color::BLUE,
+ );
+ spawn_character(
+ commands,
+ character_meshes,
+ effects,
+ materials,
+ selected_character_id,
+ character_id_list,
+ Transform::from_xyz(-128., -128., 0.),
+ Color::RED,
+ );
+ spawn_character(
+ commands,
+ character_meshes,
+ effects,
+ materials,
+ selected_character_id,
+ character_id_list,
+ Transform::from_xyz(0., -128., 0.),
+ Color::GREEN,
+ );
+}
diff --git a/src/main.rs b/src/main.rs
index b41d884..d2d072d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,5 @@
mod game;
+mod levels;
mod menu;
use bevy::{
@@ -14,6 +15,7 @@ use bevy_rapier2d::prelude::*;
enum AppState {
Menu,
Game,
+ Win,
}
fn main() {