From 7d2c68a0556114f304e66ba63c7cc7e42be95a15 Mon Sep 17 00:00:00 2001
From: tuxmain <tuxmain@zettascript.org>
Date: Fri, 26 Aug 2022 14:57:53 +0200
Subject: [PATCH] editor: save

---
 Cargo.lock              |   1 +
 Cargo.toml              |   1 +
 README.md               |   5 +-
 assets/game.levels.json | 461 ++++++++++++++++++++++++++++++----------
 src/editor.rs           | 123 +++++++++--
 5 files changed, 458 insertions(+), 133 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 8c52cf9..91bb56d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -939,6 +939,7 @@ dependencies = [
  "rand_distr",
  "rapier2d",
  "serde",
+ "serde_json",
  "ticktock",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 9b47967..22905a7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ bevy-inspector-egui = "0.12.1"
 bevy_mod_picking = "0.8.2"
 cpal = "0.14.0"
 hexodsp = { git = "https://github.com/WeirdConstructor/HexoDSP", default-features = false }
+serde_json = "1.0.85"
 ticktock = "0.8.0"
 
 [target."cfg(target_arch = \"wasm32\")".dependencies]
diff --git a/README.md b/README.md
index c29d372..c452b76 100644
--- a/README.md
+++ b/README.md
@@ -53,9 +53,10 @@ Edit the level `N: u32` with the command `bevyjam <N> e`.
 
 ### Editor controls
 
-* **Select handles**: left click to select, click in void to deselect, CTRL+click to select many, CTRL+A to select all
-* **Move handles**: arrows to move one step, Shift+arrows to move continuously
+* **Select**: left click to select, click in void to deselect, CTRL+click to select many, CTRL+A to select all
+* **Move selection**: arrows to move one step, Shift+arrows to move continuously
 * **Move camera**: CTRL+arrows
+* **Save**: CTRL+S
 
 ## License
 
diff --git a/assets/game.levels.json b/assets/game.levels.json
index 549c17b..bca10de 100644
--- a/assets/game.levels.json
+++ b/assets/game.levels.json
@@ -1,119 +1,346 @@
 {
-	"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."
-				}
-			]
-		}
-	]
+  "levels": [
+    {
+      "comment": "Movement tutorial",
+      "characters": [
+        {
+          "pos": [
+            0.0,
+            -192.0
+          ],
+          "color": [
+            1.0,
+            0.0,
+            0.0,
+            1.0
+          ]
+        },
+        {
+          "pos": [
+            -128.0,
+            -192.0
+          ],
+          "color": [
+            0.0,
+            1.0,
+            0.0,
+            1.0
+          ]
+        },
+        {
+          "pos": [
+            128.0,
+            -192.0
+          ],
+          "color": [
+            0.0,
+            0.0,
+            1.0,
+            1.0
+          ]
+        }
+      ],
+      "platforms": [
+        {
+          "pos": [
+            0.0,
+            -256.0
+          ],
+          "size": [
+            800.0,
+            16.0
+          ]
+        }
+      ],
+      "absorbing_filters": [],
+      "rotating_filters": [],
+      "texts": [
+        {
+          "pos": [
+            0.0,
+            0.0
+          ],
+          "font_size": 32.0,
+          "text": "Combine the colors to synthetize a white light.\nUse arrows to move."
+        }
+      ]
+    },
+    {
+      "comment": "Switch tutorial",
+      "characters": [
+        {
+          "pos": [
+            0.0,
+            -192.0
+          ],
+          "color": [
+            0.0,
+            1.0,
+            0.0,
+            1.0
+          ]
+        },
+        {
+          "pos": [
+            -128.0,
+            -192.0
+          ],
+          "color": [
+            1.0,
+            0.0,
+            0.0,
+            1.0
+          ]
+        },
+        {
+          "pos": [
+            128.0,
+            320.0
+          ],
+          "color": [
+            0.0,
+            0.0,
+            1.0,
+            1.0
+          ]
+        }
+      ],
+      "platforms": [
+        {
+          "pos": [
+            0.0,
+            -256.0
+          ],
+          "size": [
+            800.0,
+            16.0
+          ]
+        },
+        {
+          "pos": [
+            128.0,
+            256.0
+          ],
+          "size": [
+            96.0,
+            16.0
+          ]
+        }
+      ],
+      "absorbing_filters": [],
+      "rotating_filters": [],
+      "texts": [
+        {
+          "pos": [
+            0.0,
+            0.0
+          ],
+          "font_size": 32.0,
+          "text": "Press Tab to switch."
+        }
+      ]
+    },
+    {
+      "comment": "Absorbing filter tutorial",
+      "characters": [
+        {
+          "pos": [
+            -128.0,
+            -192.0
+          ],
+          "color": [
+            1.0,
+            0.64,
+            0.0,
+            1.0
+          ]
+        },
+        {
+          "pos": [
+            128.0,
+            -192.0
+          ],
+          "color": [
+            0.0,
+            0.37,
+            1.0,
+            1.0
+          ]
+        }
+      ],
+      "platforms": [
+        {
+          "pos": [
+            0.0,
+            -256.0
+          ],
+          "size": [
+            800.0,
+            16.0
+          ]
+        },
+        {
+          "pos": [
+            0.0,
+            -128.0
+          ],
+          "size": [
+            800.0,
+            16.0
+          ]
+        }
+      ],
+      "absorbing_filters": [
+        {
+          "pos": [
+            0.0,
+            -192.0
+          ],
+          "size": [
+            16.0,
+            112.0
+          ],
+          "color": [
+            1.0,
+            0.0,
+            0.0,
+            1.0
+          ]
+        }
+      ],
+      "rotating_filters": [],
+      "texts": [
+        {
+          "pos": [
+            0.0,
+            0.0
+          ],
+          "font_size": 32.0,
+          "text": "Press R to reset."
+        }
+      ]
+    },
+    {
+      "comment": "Rotating filter tutorial",
+      "characters": [
+        {
+          "pos": [
+            0.0,
+            -192.0
+          ],
+          "color": [
+            1.0,
+            0.0,
+            0.0,
+            1.0
+          ]
+        },
+        {
+          "pos": [
+            -128.0,
+            -192.0
+          ],
+          "color": [
+            1.0,
+            0.0,
+            0.0,
+            1.0
+          ]
+        },
+        {
+          "pos": [
+            128.0,
+            -192.0
+          ],
+          "color": [
+            1.0,
+            0.0,
+            0.0,
+            1.0
+          ]
+        }
+      ],
+      "platforms": [
+        {
+          "pos": [
+            0.0,
+            -256.0
+          ],
+          "size": [
+            800.0,
+            16.0
+          ]
+        }
+      ],
+      "absorbing_filters": [],
+      "rotating_filters": [
+        {
+          "pos": [
+            0.0,
+            -64.0
+          ],
+          "angle": 45.0
+        }
+      ],
+      "texts": [
+        {
+          "pos": [
+            0.0,
+            0.0
+          ],
+          "font_size": 32.0,
+          "text": "Let's rotate the hue!"
+        }
+      ]
+    },
+    {
+      "comment": "Game over",
+      "characters": [
+        {
+          "pos": [
+            0.0,
+            -64.0
+          ],
+          "color": [
+            1.0,
+            0.0,
+            0.0,
+            1.0
+          ]
+        }
+      ],
+      "platforms": [
+        {
+          "pos": [
+            0.0,
+            -256.0
+          ],
+          "size": [
+            800.0,
+            16.0
+          ]
+        }
+      ],
+      "absorbing_filters": [],
+      "rotating_filters": [],
+      "texts": [
+        {
+          "pos": [
+            0.0,
+            128.0
+          ],
+          "font_size": 48.0,
+          "text": "Thank you for playing!"
+        },
+        {
+          "pos": [
+            0.0,
+            0.0
+          ],
+          "font_size": 32.0,
+          "text": "There is no more light to combine."
+        }
+      ]
+    }
+  ]
 }
\ No newline at end of file
diff --git a/src/editor.rs b/src/editor.rs
index 382b585..91c24fe 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -21,6 +21,7 @@ impl Plugin for EditorPlugin {
 			.add_system_set(
 				SystemSet::on_update(AppState::Editor)
 					.with_system(move_system)
+					.with_system(input_control_system)
 					.with_system(follow_ends_system),
 			);
 	}
@@ -32,10 +33,15 @@ struct DragEndEvent(Entity);
 
 // Resources
 
+struct CharacterList(Vec<Entity>);
+
 // Components
 
 #[derive(Component)]
-struct Platform(Entity, Entity);
+struct Platform;
+
+#[derive(Component)]
+struct Ends(Entity, Entity);
 
 #[derive(Component)]
 struct Draggable;
@@ -43,12 +49,20 @@ struct Draggable;
 #[derive(Component)]
 struct End(Entity);
 
+#[derive(Component)]
+struct CharacterColor(Color);
+
+#[derive(Component)]
+struct Size(Vec2);
+
 // Bundles
 
 #[derive(Bundle)]
 struct PlatformBundle {
 	#[bundle]
 	mesh: ColorMesh2dBundle,
+	size: Size,
+	platform: Platform,
 }
 
 #[derive(Bundle)]
@@ -65,6 +79,7 @@ struct PlatformEndBundle {
 struct CharacterBundle {
 	#[bundle]
 	mesh: ColorMesh2dBundle,
+	color: CharacterColor,
 	#[bundle]
 	pickable: PickableBundle,
 	draggable: Draggable,
@@ -88,9 +103,11 @@ fn spawn_platform(
 				transform,
 				..default()
 			},
+			size: Size(size),
+			platform: Platform,
 		})
 		.id();
-	let ends = Platform(
+	let ends = Ends(
 		commands
 			.spawn_bundle(PlatformEndBundle {
 				mesh: ColorMesh2dBundle {
@@ -148,7 +165,7 @@ fn spawn_character(
 	transform: Transform,
 	color: Color,
 	index: usize,
-) {
+) -> Entity {
 	let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
 	commands
 		.spawn_bundle(CharacterBundle {
@@ -163,6 +180,7 @@ fn spawn_character(
 				transform,
 				..default()
 			},
+			color: CharacterColor(color),
 			pickable: PickableBundle::default(),
 			draggable: Draggable,
 		})
@@ -180,10 +198,11 @@ fn spawn_character(
 				transform: Transform::from_xyz(0., 0., 1.),
 				..Default::default()
 			});
-		});
+		})
+		.id()
 }
 
-pub fn spawn_stored_level(
+fn spawn_stored_level(
 	commands: &mut Commands,
 	meshes: &mut ResMut<Assets<Mesh>>,
 	materials: &mut ResMut<Assets<ColorMaterial>>,
@@ -201,8 +220,9 @@ pub fn spawn_stored_level(
 		);
 	}
 
+	let mut character_list = Vec::new();
 	for (i, character) in stored_level.characters.iter().enumerate() {
-		spawn_character(
+		character_list.push(spawn_character(
 			commands,
 			meshes,
 			materials,
@@ -210,7 +230,57 @@ pub fn spawn_stored_level(
 			Transform::from_xyz(character.pos.x, character.pos.y, 0.),
 			character.color.into(),
 			i,
-		);
+		));
+	}
+	commands.insert_resource(CharacterList(character_list));
+}
+
+fn save_level(
+	level_id: &Res<crate::game::FirstLevel>,
+	stored_levels_assets: &mut ResMut<Assets<StoredLevels>>,
+	stored_levels_handle: &Res<Handle<StoredLevels>>,
+	character_list: &Res<CharacterList>,
+	character_query: &Query<(&Transform, &CharacterColor), Without<Platform>>,
+	platform_query: &Query<(&Transform, &Size), With<Platform>>,
+) {
+	let stored_levels = stored_levels_assets.get_mut(stored_levels_handle).unwrap();
+	if let Some(stored_level) = stored_levels.levels.get_mut(level_id.0 .0 as usize) {
+		stored_level.platforms.clear();
+		for (transform, size) in platform_query.iter() {
+			stored_level.platforms.push(StoredPlatform {
+				pos: Vec2 {
+					x: transform.translation.x,
+					y: transform.translation.y,
+				},
+				size: size.0,
+			})
+		}
+
+		stored_level.characters.clear();
+		for entity in character_list.0.iter() {
+			if let Ok((transform, color)) = character_query.get(*entity) {
+				stored_level.characters.push(StoredCharacter {
+					pos: Vec2 {
+						x: transform.translation.x,
+						y: transform.translation.y,
+					},
+					color: color.0.as_rgba_f32().into(),
+				})
+			}
+		}
+	} else {
+		return;
+	}
+	match std::fs::OpenOptions::new()
+		.write(true)
+		.truncate(true)
+		.open("assets/game.levels.json")
+	{
+		Ok(mut file) => {
+			serde_json::to_writer_pretty(&mut file, stored_levels).unwrap();
+			println!("Saved!");
+		}
+		Err(e) => eprintln!("Error writing levels file: {:?}", e),
 	}
 }
 
@@ -246,6 +316,29 @@ fn setup(
 	}
 }
 
+fn input_control_system(
+	keyboard_input: Res<Input<KeyCode>>,
+	level_id: Res<crate::game::FirstLevel>,
+	mut stored_levels_assets: ResMut<Assets<StoredLevels>>,
+	stored_levels_handle: Res<Handle<StoredLevels>>,
+	character_list: Res<CharacterList>,
+	character_query: Query<(&Transform, &CharacterColor), Without<Platform>>,
+	platform_query: Query<(&Transform, &Size), With<Platform>>,
+) {
+	if keyboard_input.just_released(KeyCode::S)
+		&& (keyboard_input.pressed(KeyCode::LControl) || keyboard_input.pressed(KeyCode::RControl))
+	{
+		save_level(
+			&level_id,
+			&mut stored_levels_assets,
+			&stored_levels_handle,
+			&character_list,
+			&character_query,
+			&platform_query,
+		);
+	}
+}
+
 fn move_system(
 	keyboard_input: Res<Input<KeyCode>>,
 	mut camera_query: Query<&mut Transform, (With<Camera>, Without<Draggable>)>,
@@ -297,22 +390,24 @@ fn move_system(
 
 fn follow_ends_system(
 	mut meshes: ResMut<Assets<Mesh>>,
-	mut platform_query: Query<(&mut Transform, &mut Mesh2dHandle, &Platform)>,
-	end_query: Query<&Transform, Without<Platform>>,
+	mut follower_query: Query<(&mut Transform, &mut Mesh2dHandle, &mut Size, &Ends)>,
+	end_query: Query<&Transform, Without<Ends>>,
 	mut drag_end_event: EventReader<DragEndEvent>,
 ) {
 	for DragEndEvent(entity) in drag_end_event.iter() {
-		if let Ok((mut transform, mut mesh, Platform(end1, end2))) = platform_query.get_mut(*entity)
+		if let Ok((mut transform, mut mesh, mut size, Ends(end1, end2))) =
+			follower_query.get_mut(*entity)
 		{
 			if let (Ok(end1), Ok(end2)) = (end_query.get(*end1), end_query.get(*end2)) {
 				transform.translation.x = (end1.translation.x + end2.translation.x) / 2.;
 				transform.translation.y = (end1.translation.y + end2.translation.y) / 2.;
+				size.0 = Vec2 {
+					x: (end2.translation.x - end1.translation.x).abs(),
+					y: (end2.translation.y - end1.translation.y).abs(),
+				};
 				*mesh = meshes
 					.add(Mesh::from(Quad {
-						size: Vec2 {
-							x: (end2.translation.x - end1.translation.x).abs(),
-							y: (end2.translation.y - end1.translation.y).abs(),
-						},
+						size: size.0,
 						flip: false,
 					}))
 					.into();