diff --git a/Cargo.lock b/Cargo.lock
index 8c1947b..0105d87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -588,6 +588,25 @@ dependencies = [
"glam",
]
+[[package]]
+name = "bevy_mod_picking"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db42ac84b1409452bbfa696d9071b9a7a2505c73620c939b758b5bf23573976a"
+dependencies = [
+ "bevy",
+ "bevy_mod_raycast",
+]
+
+[[package]]
+name = "bevy_mod_raycast"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aead49a20f5e694f4fb59c7312f9a1813b65a2a0ac2c385d53d40f25cae896f"
+dependencies = [
+ "bevy",
+]
+
[[package]]
name = "bevy_pbr"
version = "0.8.1"
@@ -911,6 +930,7 @@ dependencies = [
"bevy",
"bevy-inspector-egui",
"bevy_common_assets",
+ "bevy_mod_picking",
"bevy_rapier2d",
"cpal 0.14.0",
"crossbeam-channel",
@@ -919,6 +939,7 @@ dependencies = [
"rand_distr",
"rapier2d",
"serde",
+ "serde_json",
"ticktock",
]
@@ -1563,9 +1584,9 @@ dependencies = [
[[package]]
name = "erased-serde"
-version = "0.3.22"
+version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "003000e712ad0f95857bd4d2ef8d1890069e06554101697d12050668b2f6f020"
+checksum = "54558e0ba96fbe24280072642eceb9d7d442e32c7ec0ea9e7ecd7b4ea2cf4e11"
dependencies = [
"serde",
]
diff --git a/Cargo.toml b/Cargo.toml
index cb38147..8fb6f94 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,6 @@ 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"
@@ -17,8 +16,11 @@ rapier2d = "0.14.0"
serde = { version = "1.0.144", features = ["derive"] }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
+bevy-inspector-egui = "0.12.1"
+bevy_mod_picking = "0.9.0"
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 17112e6..c452b76 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,19 @@ This game uses [HexoDSP](https://github.com/WeirdConstructor/HexoDSP) for audio
The synthetizer matrix can be edited using [HexoSynth](https://github.com/WeirdConstructor/HexoSynth) visual editor.
+## Develop
+
+Skip to level `N: u32` with the command `bevyjam <N>`.
+
+Edit the level `N: u32` with the command `bevyjam <N> e`.
+
+### Editor controls
+
+* **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
GNU AGPL v3, CopyLeft 2022 Pascal Engélibert, Nixon Cheng
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/build-wasm.sh b/build-wasm.sh
index b9643dc..0a2d039 100644
--- a/build-wasm.sh
+++ b/build-wasm.sh
@@ -1,2 +1,3 @@
-cargo build --release --target wasm32-unknown-unknown
-wasm-bindgen --out-name bevyjam --out-dir target --target web target/wasm32-unknown-unknown/release/bevyjam.wasm
+cargo build --release --target wasm32-unknown-unknown || exit 1
+echo "==> wasm-bindgen..."
+wasm-bindgen --out-name bevyjam --out-dir target --target web target/wasm32-unknown-unknown/release/bevyjam.wasm || exit 1
diff --git a/src/editor.rs b/src/editor.rs
new file mode 100644
index 0000000..55c1592
--- /dev/null
+++ b/src/editor.rs
@@ -0,0 +1,613 @@
+#![allow(clippy::type_complexity)]
+use crate::{levels::stored::*, AppState};
+
+use bevy::{
+ input::{keyboard::KeyCode, Input},
+ prelude::{
+ shape::{Circle, Quad},
+ *,
+ },
+ sprite::Mesh2dHandle,
+};
+use bevy_mod_picking::*;
+
+pub struct EditorPlugin;
+
+impl Plugin for EditorPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<DragEndEvent>()
+ .add_plugins(DefaultPickingPlugins)
+ .add_system_set(SystemSet::on_enter(AppState::Editor).with_system(setup))
+ .add_system_set(
+ SystemSet::on_update(AppState::Editor)
+ .with_system(move_system)
+ .with_system(input_control_system)
+ .with_system(follow_ends_system),
+ );
+ }
+}
+
+// Events
+
+struct DragEndEvent(Entity);
+
+// Resources
+
+struct CharacterList(Vec<Entity>);
+
+// Components
+
+#[derive(Component)]
+struct Platform;
+
+#[derive(Component)]
+struct Ends(Entity, Entity);
+
+#[derive(Component)]
+struct Draggable;
+
+#[derive(Component)]
+struct End(Entity);
+
+#[derive(Component)]
+struct CharacterColor(Color);
+
+#[derive(Component)]
+struct Size(Vec2);
+
+#[derive(Component)]
+struct RotatingFilterAngle(f32);
+
+#[derive(Component)]
+struct AbsorbingFilterColor(Color);
+
+// Bundles
+
+#[derive(Bundle)]
+struct PlatformBundle {
+ #[bundle]
+ mesh: ColorMesh2dBundle,
+ size: Size,
+ platform: Platform,
+}
+
+#[derive(Bundle)]
+struct EndBundle {
+ #[bundle]
+ mesh: ColorMesh2dBundle,
+ #[bundle]
+ pickable: PickableBundle,
+ draggable: Draggable,
+ end: End,
+}
+
+#[derive(Bundle)]
+struct CharacterBundle {
+ #[bundle]
+ mesh: ColorMesh2dBundle,
+ color: CharacterColor,
+ #[bundle]
+ pickable: PickableBundle,
+ draggable: Draggable,
+}
+
+#[derive(Bundle)]
+struct AbsorbingFilterBundle {
+ #[bundle]
+ mesh: ColorMesh2dBundle,
+ size: Size,
+ color: AbsorbingFilterColor,
+}
+
+#[derive(Bundle)]
+struct RotatingFilterBundle {
+ #[bundle]
+ mesh: ColorMesh2dBundle,
+ angle: RotatingFilterAngle,
+ #[bundle]
+ pickable: PickableBundle,
+ draggable: Draggable,
+}
+
+// Functions
+
+fn spawn_platform(
+ commands: &mut Commands,
+ meshes: &mut ResMut<Assets<Mesh>>,
+ materials: &mut ResMut<Assets<ColorMaterial>>,
+
+ transform: Transform,
+ size: Vec2,
+) {
+ let platform = commands
+ .spawn_bundle(PlatformBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes.add(Mesh::from(Quad { size, flip: false })).into(),
+ material: materials.add(ColorMaterial::from(Color::GRAY)),
+ transform,
+ ..default()
+ },
+ size: Size(size),
+ platform: Platform,
+ })
+ .id();
+ let ends = Ends(
+ commands
+ .spawn_bundle(EndBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes
+ .add(Mesh::from(Circle {
+ radius: 8.,
+ vertices: 12,
+ }))
+ .into(),
+ material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
+ transform: Transform::from_xyz(
+ transform.translation.x - size.x / 2.,
+ transform.translation.y - size.y / 2.,
+ 0.5,
+ ),
+ ..default()
+ },
+ pickable: PickableBundle::default(),
+ draggable: Draggable,
+ end: End(platform),
+ })
+ .id(),
+ commands
+ .spawn_bundle(EndBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes
+ .add(Mesh::from(Circle {
+ radius: 8.,
+ vertices: 12,
+ }))
+ .into(),
+ material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
+ transform: Transform::from_xyz(
+ transform.translation.x + size.x / 2.,
+ transform.translation.y + size.y / 2.,
+ 0.5,
+ ),
+ ..default()
+ },
+ pickable: PickableBundle::default(),
+ draggable: Draggable,
+ end: End(platform),
+ })
+ .id(),
+ );
+ commands.entity(platform).insert(ends);
+}
+
+fn spawn_character(
+ commands: &mut Commands,
+ meshes: &mut ResMut<Assets<Mesh>>,
+ materials: &mut ResMut<Assets<ColorMaterial>>,
+ asset_server: &Res<AssetServer>,
+
+ transform: Transform,
+ color: Color,
+ index: usize,
+) -> Entity {
+ let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
+ commands
+ .spawn_bundle(CharacterBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes
+ .add(Mesh::from(Quad {
+ size: Vec2 { x: 64., y: 64. },
+ flip: false,
+ }))
+ .into(),
+ material: materials.add(ColorMaterial::from(color)),
+ transform,
+ ..default()
+ },
+ color: CharacterColor(color),
+ pickable: PickableBundle::default(),
+ draggable: Draggable,
+ })
+ .with_children(|c| {
+ c.spawn_bundle(Text2dBundle {
+ text: Text::from_section(
+ &index.to_string(),
+ TextStyle {
+ font: font.clone(),
+ font_size: 32.,
+ color: Color::WHITE,
+ },
+ )
+ .with_alignment(TextAlignment::CENTER),
+ transform: Transform::from_xyz(0., 0., 1.),
+ ..Default::default()
+ });
+ })
+ .id()
+}
+
+fn spawn_absorbing_filter(
+ commands: &mut Commands,
+ meshes: &mut ResMut<Assets<Mesh>>,
+ materials: &mut ResMut<Assets<ColorMaterial>>,
+
+ transform: Transform,
+ size: Vec2,
+ color: Color,
+) {
+ let absorbing_filter = commands
+ .spawn_bundle(AbsorbingFilterBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes.add(Mesh::from(Quad { size, flip: false })).into(),
+ material: materials.add(ColorMaterial::from(color)),
+ transform,
+ ..default()
+ },
+ size: Size(size),
+ color: AbsorbingFilterColor(color),
+ })
+ .id();
+ let ends = Ends(
+ commands
+ .spawn_bundle(EndBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes
+ .add(Mesh::from(Circle {
+ radius: 8.,
+ vertices: 12,
+ }))
+ .into(),
+ material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
+ transform: Transform::from_xyz(
+ transform.translation.x - size.x / 2.,
+ transform.translation.y - size.y / 2.,
+ 0.5,
+ ),
+ ..default()
+ },
+ pickable: PickableBundle::default(),
+ draggable: Draggable,
+ end: End(absorbing_filter),
+ })
+ .id(),
+ commands
+ .spawn_bundle(EndBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes
+ .add(Mesh::from(Circle {
+ radius: 8.,
+ vertices: 12,
+ }))
+ .into(),
+ material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
+ transform: Transform::from_xyz(
+ transform.translation.x + size.x / 2.,
+ transform.translation.y + size.y / 2.,
+ 0.5,
+ ),
+ ..default()
+ },
+ pickable: PickableBundle::default(),
+ draggable: Draggable,
+ end: End(absorbing_filter),
+ })
+ .id(),
+ );
+ commands.entity(absorbing_filter).insert(ends);
+}
+
+fn spawn_rotating_filter(
+ commands: &mut Commands,
+ meshes: &mut ResMut<Assets<Mesh>>,
+ materials: &mut ResMut<Assets<ColorMaterial>>,
+ asset_server: &Res<AssetServer>,
+
+ transform: Transform,
+ angle: f32,
+) -> Entity {
+ let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
+ commands
+ .spawn_bundle(RotatingFilterBundle {
+ mesh: ColorMesh2dBundle {
+ mesh: meshes
+ .add(Mesh::from(Circle {
+ radius: 32.,
+ vertices: 36,
+ }))
+ .into(),
+ material: materials.add(ColorMaterial::from(Color::WHITE)),
+ transform,
+ ..default()
+ },
+ angle: RotatingFilterAngle(angle),
+ pickable: PickableBundle::default(),
+ draggable: Draggable,
+ })
+ .with_children(|c| {
+ c.spawn_bundle(Text2dBundle {
+ text: Text::from_section(
+ &angle.to_string(),
+ TextStyle {
+ font: font.clone(),
+ font_size: 32.,
+ color: Color::RED,
+ },
+ )
+ .with_alignment(TextAlignment::CENTER),
+ transform: Transform::from_xyz(0., 0., 1.),
+ ..Default::default()
+ });
+ })
+ .id()
+}
+
+fn spawn_stored_level(
+ commands: &mut Commands,
+ meshes: &mut ResMut<Assets<Mesh>>,
+ materials: &mut ResMut<Assets<ColorMaterial>>,
+ asset_server: &Res<AssetServer>,
+
+ stored_level: &StoredLevel,
+) {
+ for platform in stored_level.platforms.iter() {
+ spawn_platform(
+ commands,
+ meshes,
+ materials,
+ Transform::from_xyz(platform.pos.x, platform.pos.y, 0.),
+ platform.size,
+ );
+ }
+
+ let mut character_list = Vec::new();
+ for (i, character) in stored_level.characters.iter().enumerate() {
+ character_list.push(spawn_character(
+ commands,
+ meshes,
+ materials,
+ asset_server,
+ Transform::from_xyz(character.pos.x, character.pos.y, 0.),
+ character.color.into(),
+ i,
+ ));
+ }
+ commands.insert_resource(CharacterList(character_list));
+
+ 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, 0.),
+ absorbing_filter.size,
+ absorbing_filter.color.into(),
+ );
+ }
+
+ for rotating_filter in stored_level.rotating_filters.iter() {
+ spawn_rotating_filter(
+ commands,
+ meshes,
+ materials,
+ asset_server,
+ Transform::from_xyz(rotating_filter.pos.x, rotating_filter.pos.y, 0.),
+ rotating_filter.angle,
+ );
+ }
+}
+
+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>>,
+ absorbing_filter_query: &Query<(&Transform, &Size, &AbsorbingFilterColor), Without<Platform>>,
+ rotating_filter_query: &Query<
+ (&Transform, &RotatingFilterAngle),
+ (Without<Platform>, Without<CharacterColor>),
+ >,
+) {
+ 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.into(),
+ })
+ }
+ }
+
+ stored_level.absorbing_filters.clear();
+ for (transform, size, color) in absorbing_filter_query.iter() {
+ stored_level.absorbing_filters.push(StoredAbsorbingFilter {
+ pos: Vec2 {
+ x: transform.translation.x,
+ y: transform.translation.y,
+ },
+ size: size.0,
+ color: color.0.into(),
+ })
+ }
+
+ stored_level.rotating_filters.clear();
+ for (transform, angle) in rotating_filter_query.iter() {
+ stored_level.rotating_filters.push(StoredRotatingFilter {
+ pos: Vec2 {
+ x: transform.translation.x,
+ y: transform.translation.y,
+ },
+ angle: angle.0,
+ })
+ }
+ } 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),
+ }
+}
+
+// Systems
+
+fn setup(
+ mut commands: Commands,
+ mut meshes: ResMut<Assets<Mesh>>,
+ mut materials: ResMut<Assets<ColorMaterial>>,
+ camera_query: Query<Entity, With<Camera>>,
+ level_id: Res<crate::game::FirstLevel>,
+ stored_levels_assets: Res<Assets<StoredLevels>>,
+ stored_levels_handle: Res<Handle<StoredLevels>>,
+ asset_server: Res<AssetServer>,
+) {
+ commands
+ .entity(camera_query.single())
+ .insert_bundle(PickingCameraBundle::default());
+
+ if let Some(stored_level) = stored_levels_assets
+ .get(&stored_levels_handle)
+ .unwrap()
+ .levels
+ .get(level_id.0 .0 as usize)
+ {
+ spawn_stored_level(
+ &mut commands,
+ &mut meshes,
+ &mut materials,
+ &asset_server,
+ stored_level,
+ );
+ }
+}
+
+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>>,
+ absorbing_filter_query: Query<(&Transform, &Size, &AbsorbingFilterColor), Without<Platform>>,
+ rotating_filter_query: Query<
+ (&Transform, &RotatingFilterAngle),
+ (Without<Platform>, Without<CharacterColor>),
+ >,
+) {
+ 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,
+ &absorbing_filter_query,
+ &rotating_filter_query,
+ );
+ }
+}
+
+fn move_system(
+ keyboard_input: Res<Input<KeyCode>>,
+ mut camera_query: Query<&mut Transform, (With<Camera>, Without<Draggable>)>,
+ mut drag_query: Query<(&mut Transform, &Selection, Option<&End>), With<Draggable>>,
+ mut drag_end_event: EventWriter<DragEndEvent>,
+) {
+ if keyboard_input.pressed(KeyCode::LControl) || keyboard_input.pressed(KeyCode::RControl) {
+ let mut transform = camera_query.single_mut();
+ let drag = Vec3 {
+ x: (keyboard_input.pressed(KeyCode::Right) as i8
+ - keyboard_input.pressed(KeyCode::Left) as i8) as _,
+ y: (keyboard_input.pressed(KeyCode::Up) as i8
+ - keyboard_input.pressed(KeyCode::Down) as i8) as _,
+ z: 0.,
+ } * 8.;
+ transform.translation += drag;
+ return;
+ }
+
+ let drag = if keyboard_input.pressed(KeyCode::LShift) || keyboard_input.pressed(KeyCode::RShift)
+ {
+ Vec3 {
+ x: (keyboard_input.pressed(KeyCode::Right) as i8
+ - keyboard_input.pressed(KeyCode::Left) as i8) as _,
+ y: (keyboard_input.pressed(KeyCode::Up) as i8
+ - keyboard_input.pressed(KeyCode::Down) as i8) as _,
+ z: 0.,
+ }
+ } else {
+ Vec3 {
+ x: (keyboard_input.just_pressed(KeyCode::Right) as i8
+ - keyboard_input.just_pressed(KeyCode::Left) as i8) as _,
+ y: (keyboard_input.just_pressed(KeyCode::Up) as i8
+ - keyboard_input.just_pressed(KeyCode::Down) as i8) as _,
+ z: 0.,
+ }
+ } * 8.;
+ if drag != Vec3::ZERO {
+ for (mut transform, selection, end) in drag_query.iter_mut() {
+ if selection.selected() {
+ transform.translation += drag;
+ if let Some(End(entity)) = end {
+ drag_end_event.send(DragEndEvent(*entity));
+ }
+ }
+ }
+ }
+}
+
+fn follow_ends_system(
+ mut meshes: ResMut<Assets<Mesh>>,
+ 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, 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: size.0,
+ flip: false,
+ }))
+ .into();
+ }
+ }
+ }
+}
diff --git a/src/game.rs b/src/game.rs
index 102a44e..32f3bb0 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -471,14 +471,13 @@ fn move_camera(
let size: Vec2 = camera.logical_viewport_size().unwrap();
let half_height: f32 = size.y * 0.5;
- let mut target_translation = character_transform.translation;
+ let mut target_translation = character_transform.translation;
// prevent camera from going too low
target_translation.y = target_translation.y.max(half_height - MARGIN);
- camera_transform.translation = camera_transform.translation.lerp(
- target_translation,
- time.delta_seconds() * FOLLOW_SPEED,
- );
+ camera_transform.translation = camera_transform
+ .translation
+ .lerp(target_translation, time.delta_seconds() * FOLLOW_SPEED);
// always make sure that camera is away from the object in order to render them
camera_transform.translation.z = 999.0;
diff --git a/src/levels.rs b/src/levels.rs
index c131e63..f99b429 100644
--- a/src/levels.rs
+++ b/src/levels.rs
@@ -1,5 +1,7 @@
#![allow(clippy::too_many_arguments)]
+pub use stored::*;
+
use crate::game::*;
use bevy::{prelude::*, reflect::TypeUuid};
@@ -55,60 +57,6 @@ pub fn post_setup_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>,
@@ -179,3 +127,61 @@ pub fn spawn_stored_level(
.insert(Level);
}
}
+
+pub mod stored {
+ use super::*;
+
+ #[derive(Deserialize, Serialize, TypeUuid)]
+ #[uuid = "1fbba930-644b-0d62-2514-4b302b945327"]
+ pub struct StoredLevels {
+ pub 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,
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 410492a..7976caa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,9 @@
+#![allow(clippy::too_many_arguments)]
+
#[cfg(not(target_arch = "wasm32"))]
mod audio;
+#[cfg(not(target_arch = "wasm32"))]
+mod editor;
mod filters;
mod game;
mod levels;
@@ -7,6 +11,7 @@ mod menu;
mod particle_effect;
use bevy::{
+ asset::{Asset, HandleId, LoadState},
prelude::*,
window::{WindowId, WindowMode},
};
@@ -15,9 +20,22 @@ use bevy_rapier2d::prelude::*;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum AppState {
+ Loading,
Menu,
Game,
Win,
+ Editor,
+}
+
+struct UseEditor(bool);
+
+struct LoadingAssets(Vec<HandleId>);
+
+impl LoadingAssets {
+ fn add<T: Asset>(&mut self, handle: Handle<T>) -> Handle<T> {
+ self.0.push(handle.id);
+ handle
+ }
}
fn main() {
@@ -28,32 +46,45 @@ fn main() {
std::thread::spawn(move || audio::setup(audio_event_receiver));
#[cfg(not(target_arch = "wasm32"))]
- let first_level = game::LevelId(
- std::env::args()
- .nth(1)
- .map_or(0, |s| s.parse().unwrap_or(0)),
- );
+ let (first_level, use_editor) = {
+ let mut args = std::env::args().skip(1);
+ (
+ game::LevelId(args.next().map_or(0, |s| s.parse().unwrap_or(0))),
+ args.next().map_or(false, |s| s == "e"),
+ )
+ };
#[cfg(target_arch = "wasm32")]
- let first_level = game::LevelId(0);
+ let (first_level, use_editor) = (game::LevelId(0), false);
- App::new()
- .insert_resource(Msaa { samples: 4 })
+ let mut app = App::new();
+ app.insert_resource(Msaa { samples: 4 })
.insert_resource(audio_event_sender)
- .add_state(AppState::Menu)
+ .insert_resource(UseEditor(use_editor))
+ .add_state(AppState::Loading)
.insert_resource(game::FirstLevel(first_level))
.insert_resource(ClearColor(Color::BLACK))
.add_plugins(DefaultPlugins)
+ //.add_plugin(RapierDebugRenderPlugin::default())
+ //.add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new())
.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)
- .add_plugin(game::GamePlugin)
- .add_plugin(particle_effect::ParticleEffectPlugin)
- //.add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new())
- .add_system(keyboard_util_system)
+ ]));
+
+ if !use_editor {
+ app.add_plugin(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(64.0))
+ .add_plugin(menu::MenuPlugin)
+ .add_plugin(game::GamePlugin)
+ .add_plugin(particle_effect::ParticleEffectPlugin);
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ if use_editor {
+ app.add_plugin(editor::EditorPlugin);
+ }
+
+ app.add_system(keyboard_util_system)
.add_startup_system(setup)
+ .add_system_set(SystemSet::on_update(AppState::Loading).with_system(loading_system))
.run();
}
@@ -63,9 +94,13 @@ fn setup(mut commands: Commands, mut windows: ResMut<Windows>, asset_server: Res
.unwrap()
.set_title(String::from("Bevyjam"));
- 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"));
+ let mut assets = LoadingAssets(Vec::new());
+ commands.insert_resource(
+ assets.add(asset_server.load::<levels::StoredLevels, _>("game.levels.json")),
+ );
+ commands.insert_resource(assets.add(asset_server.load::<Font, _>("UacariLegacy-Thin.ttf")));
+ commands.insert_resource(assets.add(asset_server.load::<Image, _>("bevy.png")));
+ commands.insert_resource(assets);
commands.spawn_bundle(Camera2dBundle::default());
commands.insert_resource(AmbientLight {
@@ -74,6 +109,23 @@ fn setup(mut commands: Commands, mut windows: ResMut<Windows>, asset_server: Res
});
}
+fn loading_system(
+ asset_server: Res<AssetServer>,
+ use_editor: Res<UseEditor>,
+ assets: Res<LoadingAssets>,
+ mut app_state: ResMut<State<AppState>>,
+) {
+ if asset_server.get_group_load_state(assets.0.iter().copied()) == LoadState::Loaded {
+ app_state
+ .replace(if use_editor.0 {
+ AppState::Editor
+ } else {
+ AppState::Menu
+ })
+ .ok();
+ }
+}
+
fn keyboard_util_system(keyboard_input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) {
#[cfg(not(target_arch = "wasm32"))]
{
diff --git a/src/particle_effect.rs b/src/particle_effect.rs
index c1ee094..b711548 100644
--- a/src/particle_effect.rs
+++ b/src/particle_effect.rs
@@ -44,7 +44,6 @@ impl FromWorld for ParticleMesh {
}
}
-#[derive(bevy_inspector_egui::Inspectable)]
pub struct ParticleEffectResource {
pub translation: Vec3,
pub prev_translation: Vec3,