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 `. + +Edit the level `N: u32` with the command `bevyjam 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::() + .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); + +// 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>, + materials: &mut ResMut>, + + 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>, + materials: &mut ResMut>, + asset_server: &Res, + + 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>, + materials: &mut ResMut>, + + 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>, + materials: &mut ResMut>, + asset_server: &Res, + + 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>, + materials: &mut ResMut>, + asset_server: &Res, + + 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, + stored_levels_assets: &mut ResMut>, + stored_levels_handle: &Res>, + character_list: &Res, + character_query: &Query<(&Transform, &CharacterColor), Without>, + platform_query: &Query<(&Transform, &Size), With>, + absorbing_filter_query: &Query<(&Transform, &Size, &AbsorbingFilterColor), Without>, + rotating_filter_query: &Query< + (&Transform, &RotatingFilterAngle), + (Without, Without), + >, +) { + 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>, + mut materials: ResMut>, + camera_query: Query>, + level_id: Res, + stored_levels_assets: Res>, + stored_levels_handle: Res>, + asset_server: Res, +) { + 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>, + level_id: Res, + mut stored_levels_assets: ResMut>, + stored_levels_handle: Res>, + character_list: Res, + character_query: Query<(&Transform, &CharacterColor), Without>, + platform_query: Query<(&Transform, &Size), With>, + absorbing_filter_query: Query<(&Transform, &Size, &AbsorbingFilterColor), Without>, + rotating_filter_query: Query< + (&Transform, &RotatingFilterAngle), + (Without, Without), + >, +) { + 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>, + mut camera_query: Query<&mut Transform, (With, Without)>, + mut drag_query: Query<(&mut Transform, &Selection, Option<&End>), With>, + mut drag_end_event: EventWriter, +) { + 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>, + mut follower_query: Query<(&mut Transform, &mut Mesh2dHandle, &mut Size, &Ends)>, + end_query: Query<&Transform, Without>, + mut drag_end_event: EventReader, +) { + 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, -} - -#[derive(Deserialize, Serialize, TypeUuid)] -#[uuid = "a1464a30-1f57-a654-d56c-ded41032af0b"] -pub struct StoredLevel { - pub comment: String, - pub characters: Vec, - pub platforms: Vec, - pub absorbing_filters: Vec, - pub rotating_filters: Vec, - pub texts: Vec, -} - -#[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, @@ -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, + } + + #[derive(Deserialize, Serialize, TypeUuid)] + #[uuid = "a1464a30-1f57-a654-d56c-ded41032af0b"] + pub struct StoredLevel { + pub comment: String, + pub characters: Vec, + pub platforms: Vec, + pub absorbing_filters: Vec, + pub rotating_filters: Vec, + pub texts: Vec, + } + + #[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); + +impl LoadingAssets { + fn add(&mut self, handle: Handle) -> Handle { + 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::::new(&[ "levels.json", - ])) - .add_plugin(RapierPhysicsPlugin::::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::::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, asset_server: Res .unwrap() .set_title(String::from("Bevyjam")); - commands.insert_resource(asset_server.load::("game.levels.json")); - commands.insert_resource(asset_server.load::("UacariLegacy-Thin.ttf")); - commands.insert_resource(asset_server.load::("bevy.png")); + let mut assets = LoadingAssets(Vec::new()); + commands.insert_resource( + assets.add(asset_server.load::("game.levels.json")), + ); + commands.insert_resource(assets.add(asset_server.load::("UacariLegacy-Thin.ttf"))); + commands.insert_resource(assets.add(asset_server.load::("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, asset_server: Res }); } +fn loading_system( + asset_server: Res, + use_editor: Res, + assets: Res, + mut app_state: ResMut>, +) { + 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>, mut windows: ResMut) { #[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,