From 1d22c11cde12798dbbda9ec80e9999831786bffd Mon Sep 17 00:00:00 2001
From: tuxmain <tuxmain@zettascript.org>
Date: Thu, 25 Aug 2022 19:55:34 +0200
Subject: [PATCH] Levels stored outside program

---
 Cargo.lock              |  14 ++++
 Cargo.toml              |   2 +
 assets/game.levels.json | 119 ++++++++++++++++++++++++++
 src/levels.rs           | 180 ++++++++++++++++++++++++++++++----------
 src/levels/game_over.rs |  61 --------------
 src/levels/level0.rs    |  50 -----------
 src/levels/level1.rs    |  58 -------------
 src/levels/level2.rs    |  72 ----------------
 src/levels/level3.rs    |  59 -------------
 src/main.rs             |  12 +--
 10 files changed, 280 insertions(+), 347 deletions(-)
 create mode 100644 assets/game.levels.json
 delete mode 100644 src/levels/game_over.rs
 delete mode 100644 src/levels/level0.rs
 delete mode 100644 src/levels/level1.rs
 delete mode 100644 src/levels/level2.rs
 delete mode 100644 src/levels/level3.rs

diff --git a/Cargo.lock b/Cargo.lock
index 3ddac41..f26df36 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -316,6 +316,18 @@ dependencies = [
  "rodio",
 ]
 
+[[package]]
+name = "bevy_common_assets"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f7be9ee39085d8319d5cd853447b0b5c4f8b4bfd647aec91e2bd996e9db67ef"
+dependencies = [
+ "anyhow",
+ "bevy",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "bevy_core"
 version = "0.8.1"
@@ -898,6 +910,7 @@ version = "0.1.0"
 dependencies = [
  "bevy",
  "bevy-inspector-egui",
+ "bevy_common_assets",
  "bevy_rapier2d",
  "cpal 0.14.0",
  "crossbeam-channel",
@@ -905,6 +918,7 @@ dependencies = [
  "rand",
  "rand_distr",
  "rapier2d",
+ "serde",
  "ticktock",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index f50dc6f..df12e56 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,12 +7,14 @@ edition = "2021"
 
 [dependencies]
 bevy = "0.8.1"
+bevy_common_assets = { version = "0.3.0", features = ["json"] }
 bevy-inspector-egui = "0.12.1"
 bevy_rapier2d = "0.16.2"
 crossbeam-channel = "0.5.6"
 rand = "0.8.5"
 rand_distr = "0.4.3"
 rapier2d = "0.14.0"
+serde = { version = "1.0.144", features = ["derive"] }
 
 [target."cfg(not(target_arch = \"wasm32\"))".dependencies]
 cpal = "0.14.0"
diff --git a/assets/game.levels.json b/assets/game.levels.json
new file mode 100644
index 0000000..549c17b
--- /dev/null
+++ b/assets/game.levels.json
@@ -0,0 +1,119 @@
+{
+	"levels": [
+		{
+			"comment": "Movement tutorial",
+			"platforms": [
+				{"pos": [0, -256], "size": [800, 16]}
+			],
+			"characters": [
+				{"pos": [0, -192], "color": [1,0,0,1]},
+				{"pos": [-128, -192], "color": [0,1,0,1]},
+				{"pos": [128, -192], "color": [0,0,1,1]}
+			],
+			"absorbing_filters": [],
+			"rotating_filters": [],
+			"texts": [
+				{
+					"pos": [0, 0],
+					"font_size": 32,
+					"text": "Combine the colors to synthetize a white light.\nUse arrows to move."
+				}
+			]
+		},
+		{
+			"comment": "Switch tutorial",
+			"platforms": [
+				{"pos": [0, -256], "size": [800, 16]},
+				{"pos": [128, 256], "size": [96, 16]}
+			],
+			"characters": [
+				{"pos": [0, -192], "color": [0,1,0,1]},
+				{"pos": [-128, -192], "color": [1,0,0,1]},
+				{"pos": [128, 320], "color": [0,0,1,1]}
+			],
+			"absorbing_filters": [],
+			"rotating_filters": [],
+			"texts": [
+				{
+					"pos": [0, 0],
+					"font_size": 32,
+					"text": "Press Tab to switch."
+				}
+			]
+		},
+		{
+			"comment": "Absorbing filter tutorial",
+			"platforms": [
+				{"pos": [0, -256], "size": [800, 16]},
+				{"pos": [0, -128], "size": [800, 16]}
+			],
+			"characters": [
+				{"pos": [-128, -192], "color": [1,0.64,0,1]},
+				{"pos": [128, -192], "color": [0,0.37,1,1]}
+			],
+			"absorbing_filters": [
+				{
+					"pos": [0, -192],
+					"size": [16, 112],
+					"color": [1,0,0,1]
+				}
+			],
+			"rotating_filters": [],
+			"texts": [
+				{
+					"pos": [0, 0],
+					"font_size": 32,
+					"text": "Press R to reset."
+				}
+			]
+		},
+		{
+			"comment": "Rotating filter tutorial",
+			"platforms": [
+				{"pos": [0, -256], "size": [800, 16]}
+			],
+			"characters": [
+				{"pos": [0, -192], "color": [1,0,0,1]},
+				{"pos": [-128, -192], "color": [1,0,0,1]},
+				{"pos": [128, -192], "color": [1,0,0,1]}
+			],
+			"absorbing_filters": [],
+			"rotating_filters": [
+				{
+					"pos": [0, -64],
+					"angle": 45
+				}
+			],
+			"texts": [
+				{
+					"pos": [0, 0],
+					"font_size": 32,
+					"text": "Let's rotate the hue!"
+				}
+			]
+		},
+		{
+			"comment": "Game over",
+			"platforms": [
+				{"pos": [0, -256], "size": [800, 16]}
+			],
+			"characters": [
+				{"pos": [0, -64], "color": [1,0,0,1]}
+			],
+			"absorbing_filters": [],
+			"rotating_filters": [],
+			"texts": [
+				{
+					"pos": [0, 128],
+					"font_size": 48,
+					"text": "Thank you for playing!"
+				},
+				{
+					"pos": [0, 0],
+					"font_size": 32,
+					"text": "There is no more light to combine."
+				}
+			]
+		}
+	]
+}
\ No newline at end of file
diff --git a/src/levels.rs b/src/levels.rs
index ad217e1..c131e63 100644
--- a/src/levels.rs
+++ b/src/levels.rs
@@ -1,14 +1,9 @@
 #![allow(clippy::too_many_arguments)]
 
-mod game_over;
-mod level0;
-mod level1;
-mod level2;
-mod level3;
-
 use crate::game::*;
 
-use bevy::prelude::*;
+use bevy::{prelude::*, reflect::TypeUuid};
+use serde::{Deserialize, Serialize};
 
 pub fn setup_level(
 	level_startup_event: &mut EventWriter<LevelStartupEvent>,
@@ -35,51 +30,152 @@ pub fn post_setup_level(
 	mut level_startup_event: EventReader<LevelStartupEvent>,
 	asset_server: Res<AssetServer>,
 	audio: Res<crossbeam_channel::Sender<AudioMsg>>,
+	stored_levels_assets: Res<Assets<StoredLevels>>,
+	stored_levels_handle: Res<Handle<StoredLevels>>,
 ) {
 	for _ in level_startup_event.iter() {
 		if let Some(level_id) = current_level.0 {
-			match level_id.0 {
-				0 => level0::setup(
+			if let Some(stored_level) = stored_levels_assets
+				.get(&stored_levels_handle)
+				.unwrap()
+				.levels
+				.get(level_id.0 as usize)
+			{
+				spawn_stored_level(
 					&mut commands,
-					&mut meshes,
 					&character_meshes,
-					&mut materials,
-					&audio,
-					&asset_server,
-				),
-				1 => level1::setup(
-					&mut commands,
 					&mut meshes,
-					&character_meshes,
 					&mut materials,
-					&audio,
 					&asset_server,
-				),
-				2 => level2::setup(
-					&mut commands,
-					&mut meshes,
-					&character_meshes,
-					&mut materials,
 					&audio,
-					&asset_server,
-				),
-				3 => level3::setup(
-					&mut commands,
-					&mut meshes,
-					&character_meshes,
-					&mut materials,
-					&audio,
-					&asset_server,
-				),
-				_ => game_over::setup(
-					&mut commands,
-					&mut meshes,
-					&character_meshes,
-					&mut materials,
-					&audio,
-					&asset_server,
-				),
+					stored_level,
+				);
 			}
 		}
 	}
 }
+
+#[derive(Deserialize, Serialize, TypeUuid)]
+#[uuid = "1fbba930-644b-0d62-2514-4b302b945327"]
+pub struct StoredLevels {
+	levels: Vec<StoredLevel>,
+}
+
+#[derive(Deserialize, Serialize, TypeUuid)]
+#[uuid = "a1464a30-1f57-a654-d56c-ded41032af0b"]
+pub struct StoredLevel {
+	pub comment: String,
+	pub characters: Vec<StoredCharacter>,
+	pub platforms: Vec<StoredPlatform>,
+	pub absorbing_filters: Vec<StoredAbsorbingFilter>,
+	pub rotating_filters: Vec<StoredRotatingFilter>,
+	pub texts: Vec<StoredText>,
+}
+
+#[derive(Deserialize, Serialize, TypeUuid)]
+#[uuid = "1c798f8c-ef15-c528-693e-76becdef6b10"]
+pub struct StoredCharacter {
+	pub pos: Vec2,
+	pub color: Vec4,
+}
+
+#[derive(Deserialize, Serialize, TypeUuid)]
+#[uuid = "31696095-59de-93be-b5e9-333c2afbc900"]
+pub struct StoredPlatform {
+	pub pos: Vec2,
+	pub size: Vec2,
+}
+
+#[derive(Deserialize, Serialize, TypeUuid)]
+#[uuid = "bcad7fff-0605-c4e3-3cd4-42d5bbaad926"]
+pub struct StoredAbsorbingFilter {
+	pub pos: Vec2,
+	pub size: Vec2,
+	pub color: Vec4,
+}
+
+#[derive(Deserialize, Serialize, TypeUuid)]
+#[uuid = "fa2843f2-6e34-601b-6c46-4827b0370b3f"]
+pub struct StoredRotatingFilter {
+	pub pos: Vec2,
+	pub angle: f32,
+}
+
+#[derive(Deserialize, Serialize, TypeUuid)]
+#[uuid = "72f6321a-f01f-6eea-9b17-3159837a2fd3"]
+pub struct StoredText {
+	pub pos: Vec2,
+	pub font_size: f32,
+	pub text: String,
+}
+
+pub fn spawn_stored_level(
+	commands: &mut Commands,
+	character_meshes: &Res<CharacterMeshes>,
+	meshes: &mut ResMut<Assets<Mesh>>,
+	materials: &mut ResMut<Assets<ColorMaterial>>,
+	asset_server: &Res<AssetServer>,
+	audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
+
+	stored_level: &StoredLevel,
+) {
+	let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
+	spawn_platforms(
+		commands,
+		meshes,
+		materials,
+		stored_level.platforms.iter().map(|platform| {
+			(
+				Transform::from_xyz(platform.pos.x, platform.pos.y, 0.),
+				platform.size,
+			)
+		}),
+	);
+	spawn_characters(
+		commands,
+		character_meshes,
+		materials,
+		audio,
+		stored_level.characters.iter().map(|character| {
+			(
+				Transform::from_xyz(character.pos.x, character.pos.y, 0.),
+				character.color.into(),
+			)
+		}),
+	);
+	for absorbing_filter in stored_level.absorbing_filters.iter() {
+		spawn_absorbing_filter(
+			commands,
+			meshes,
+			materials,
+			Transform::from_xyz(absorbing_filter.pos.x, absorbing_filter.pos.y, 2.),
+			absorbing_filter.size,
+			absorbing_filter.color.into(),
+		);
+	}
+	for rotating_filter in stored_level.rotating_filters.iter() {
+		spawn_rotating_filter(
+			commands,
+			asset_server,
+			Transform::from_xyz(rotating_filter.pos.x, rotating_filter.pos.y, 2.),
+			rotating_filter.angle,
+		);
+	}
+	for text in stored_level.texts.iter() {
+		commands
+			.spawn_bundle(Text2dBundle {
+				text: Text::from_section(
+					&text.text,
+					TextStyle {
+						font: font.clone(),
+						font_size: text.font_size,
+						color: Color::WHITE,
+					},
+				)
+				.with_alignment(TextAlignment::CENTER),
+				transform: Transform::from_xyz(text.pos.x, text.pos.y, 0.),
+				..Default::default()
+			})
+			.insert(Level);
+	}
+}
diff --git a/src/levels/game_over.rs b/src/levels/game_over.rs
deleted file mode 100644
index d513383..0000000
--- a/src/levels/game_over.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-use crate::game::*;
-
-use bevy::prelude::*;
-
-pub fn setup(
-	commands: &mut Commands,
-	meshes: &mut ResMut<Assets<Mesh>>,
-	character_meshes: &Res<CharacterMeshes>,
-	materials: &mut ResMut<Assets<ColorMaterial>>,
-	audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
-	asset_server: &Res<AssetServer>,
-) {
-	let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
-	commands
-		.spawn_bundle(Text2dBundle {
-			text: Text::from_section(
-				"Thank you for playing!",
-				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);
-
-	spawn_platform(
-		commands,
-		meshes,
-		materials,
-		Transform::from_xyz(0.0, -256.0, 0.0),
-		Vec2 { x: 800.0, y: 16.0 },
-	);
-
-	spawn_character(
-		commands,
-		character_meshes,
-		materials,
-		audio,
-		Transform::from_xyz(0., -64., 0.),
-		Color::RED,
-		true,
-	);
-}
diff --git a/src/levels/level0.rs b/src/levels/level0.rs
deleted file mode 100644
index d6789d8..0000000
--- a/src/levels/level0.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Movement tutorial
-
-use crate::game::*;
-
-use bevy::prelude::*;
-
-pub fn setup(
-	commands: &mut Commands,
-	meshes: &mut ResMut<Assets<Mesh>>,
-	character_meshes: &Res<CharacterMeshes>,
-	materials: &mut ResMut<Assets<ColorMaterial>>,
-	audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
-	asset_server: &Res<AssetServer>,
-) {
-	let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
-	commands
-		.spawn_bundle(Text2dBundle {
-			text: Text::from_section(
-				"Combine the colors to synthetize a white light.\nUse arrows to move.",
-				TextStyle {
-					font,
-					font_size: 32.0,
-					color: Color::WHITE,
-				},
-			)
-			.with_alignment(TextAlignment::CENTER),
-			..Default::default()
-		})
-		.insert(Level);
-
-	spawn_platform(
-		commands,
-		meshes,
-		materials,
-		Transform::from_xyz(0.0, -256.0, 0.0),
-		Vec2 { x: 800.0, y: 16.0 },
-	);
-
-	spawn_characters(
-		commands,
-		character_meshes,
-		materials,
-		audio,
-		[
-			(Transform::from_xyz(0., -192., 0.), Color::RED),
-			(Transform::from_xyz(-128., -192., 0.), Color::GREEN),
-			(Transform::from_xyz(128., -192., 0.), Color::BLUE),
-		],
-	);
-}
diff --git a/src/levels/level1.rs b/src/levels/level1.rs
deleted file mode 100644
index 1d92187..0000000
--- a/src/levels/level1.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-// Switch tutorial
-
-use crate::game::*;
-
-use bevy::prelude::*;
-
-pub fn setup(
-	commands: &mut Commands,
-	meshes: &mut ResMut<Assets<Mesh>>,
-	character_meshes: &Res<CharacterMeshes>,
-	materials: &mut ResMut<Assets<ColorMaterial>>,
-	audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
-	asset_server: &Res<AssetServer>,
-) {
-	let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
-	commands
-		.spawn_bundle(Text2dBundle {
-			text: Text::from_section(
-				"Press Tab to switch.",
-				TextStyle {
-					font,
-					font_size: 32.0,
-					color: Color::WHITE,
-				},
-			)
-			.with_alignment(TextAlignment::CENTER),
-			..Default::default()
-		})
-		.insert(Level);
-
-	spawn_platforms(
-		commands,
-		meshes,
-		materials,
-		[
-			(
-				Transform::from_xyz(0.0, -256.0, 0.0),
-				Vec2 { x: 800.0, y: 16.0 },
-			),
-			(
-				Transform::from_xyz(128.0, 256.0, 0.0),
-				Vec2 { x: 96.0, y: 16.0 },
-			),
-		],
-	);
-
-	spawn_characters(
-		commands,
-		character_meshes,
-		materials,
-		audio,
-		[
-			(Transform::from_xyz(0., -192., 0.), Color::GREEN),
-			(Transform::from_xyz(-128., -192., 0.), Color::RED),
-			(Transform::from_xyz(128., 320., 0.), Color::BLUE),
-		],
-	);
-}
diff --git a/src/levels/level2.rs b/src/levels/level2.rs
deleted file mode 100644
index 91606d5..0000000
--- a/src/levels/level2.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-// Absorbing filter tutorial
-
-use crate::game::*;
-
-use bevy::prelude::*;
-
-pub fn setup(
-	commands: &mut Commands,
-	meshes: &mut ResMut<Assets<Mesh>>,
-	character_meshes: &Res<CharacterMeshes>,
-	materials: &mut ResMut<Assets<ColorMaterial>>,
-	audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
-	asset_server: &Res<AssetServer>,
-) {
-	let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
-	commands
-		.spawn_bundle(Text2dBundle {
-			text: Text::from_section(
-				"Press R to reset.",
-				TextStyle {
-					font,
-					font_size: 32.0,
-					color: Color::WHITE,
-				},
-			)
-			.with_alignment(TextAlignment::CENTER),
-			..Default::default()
-		})
-		.insert(Level);
-
-	spawn_platforms(
-		commands,
-		meshes,
-		materials,
-		[
-			(
-				Transform::from_xyz(0.0, -256.0, 0.0),
-				Vec2 { x: 800.0, y: 16.0 },
-			),
-			(
-				Transform::from_xyz(0.0, -128.0, 0.0),
-				Vec2 { x: 800.0, y: 16.0 },
-			),
-		],
-	);
-
-	spawn_characters(
-		commands,
-		character_meshes,
-		materials,
-		audio,
-		[
-			(
-				Transform::from_xyz(-128., -192., 0.),
-				Color::rgba(1., 0.64, 0., 1.),
-			),
-			(
-				Transform::from_xyz(128., -192., 0.),
-				Color::rgba(0., 0.37, 1., 1.),
-			),
-		],
-	);
-
-	spawn_absorbing_filter(
-		commands,
-		meshes,
-		materials,
-		Transform::from_xyz(0., -192., 2.),
-		Vec2 { x: 16.0, y: 112.0 },
-		Color::RED,
-	);
-}
diff --git a/src/levels/level3.rs b/src/levels/level3.rs
deleted file mode 100644
index 5fa2607..0000000
--- a/src/levels/level3.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Rotating filter tutorial
-
-use crate::game::*;
-
-use bevy::prelude::*;
-
-pub fn setup(
-	commands: &mut Commands,
-	meshes: &mut ResMut<Assets<Mesh>>,
-	character_meshes: &Res<CharacterMeshes>,
-	materials: &mut ResMut<Assets<ColorMaterial>>,
-	audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
-	asset_server: &Res<AssetServer>,
-) {
-	let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
-	commands
-		.spawn_bundle(Text2dBundle {
-			text: Text::from_section(
-				"Let's rotate the hue!",
-				TextStyle {
-					font,
-					font_size: 32.0,
-					color: Color::WHITE,
-				},
-			)
-			.with_alignment(TextAlignment::CENTER),
-			..Default::default()
-		})
-		.insert(Level);
-
-	spawn_platforms(
-		commands,
-		meshes,
-		materials,
-		[(
-			Transform::from_xyz(0.0, -256.0, 0.0),
-			Vec2 { x: 800.0, y: 16.0 },
-		)],
-	);
-
-	spawn_characters(
-		commands,
-		character_meshes,
-		materials,
-		audio,
-		[
-			(Transform::from_xyz(0., -192., 0.), Color::RED),
-			(Transform::from_xyz(-128., -192., 0.), Color::RED),
-			(Transform::from_xyz(128., -192., 0.), Color::RED),
-		],
-	);
-
-	spawn_rotating_filter(
-		commands,
-		asset_server,
-		Transform::from_xyz(0., -64., 2.),
-		45.,
-	);
-}
diff --git a/src/main.rs b/src/main.rs
index 6cb80d7..410492a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,6 +10,7 @@ use bevy::{
 	prelude::*,
 	window::{WindowId, WindowMode},
 };
+use bevy_common_assets::json::JsonAssetPlugin;
 use bevy_rapier2d::prelude::*;
 
 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -42,6 +43,9 @@ fn main() {
 		.insert_resource(game::FirstLevel(first_level))
 		.insert_resource(ClearColor(Color::BLACK))
 		.add_plugins(DefaultPlugins)
+		.add_plugin(JsonAssetPlugin::<levels::StoredLevels>::new(&[
+			"levels.json",
+		]))
 		.add_plugin(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(64.0))
 		//.add_plugin(RapierDebugRenderPlugin::default())
 		.add_plugin(menu::MenuPlugin)
@@ -59,11 +63,9 @@ fn setup(mut commands: Commands, mut windows: ResMut<Windows>, asset_server: Res
 		.unwrap()
 		.set_title(String::from("Bevyjam"));
 
-	let font: Handle<Font> = asset_server.load("UacariLegacy-Thin.ttf");
-	commands.insert_resource(font);
-
-	let bevy_icon: Handle<Image> = asset_server.load("bevy.png");
-	commands.insert_resource(bevy_icon);
+	commands.insert_resource(asset_server.load::<levels::StoredLevels, _>("game.levels.json"));
+	commands.insert_resource(asset_server.load::<Font, _>("UacariLegacy-Thin.ttf"));
+	commands.insert_resource(asset_server.load::<Image, _>("bevy.png"));
 
 	commands.spawn_bundle(Camera2dBundle::default());
 	commands.insert_resource(AmbientLight {