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() {