use std::path::Path; use bevy::color::palettes::css; use bevy::ecs::system::SystemState; use bevy::prelude::*; use bevy::render::mesh::Indices; use bevy::render::render_asset::RenderAssetUsages; use bevy::render::render_resource::PrimitiveTopology; use bevy::sprite::Mesh2dHandle; use bevy_egui::{egui, EguiContexts, EguiPlugin}; use uuid::Uuid; use bevy_yoleck::exclusive_systems::{YoleckExclusiveSystemDirective, YoleckExclusiveSystemsQueue}; use bevy_yoleck::vpeol::{prelude::*, vpeol_read_click_on_entity}; use bevy_yoleck::{ prelude::*, yoleck_exclusive_system_cancellable, yoleck_map_entity_to_uuid, YoleckDirective, }; use serde::{Deserialize, Serialize}; fn main() { let mut app = App::new(); app.add_plugins(DefaultPlugins); let level = std::env::args().nth(1); if let Some(level) = level { // The egui plugin is not needed for the game itself, but GameAssets won't load without it // because it needs `EguiContexts` which cannot be `Option` because it's a custom // `SystemParam`. app.add_plugins(EguiPlugin); app.add_plugins(YoleckPluginForGame); app.add_systems( Startup, move |asset_server: Res, mut commands: Commands| { commands.spawn(YoleckLoadLevel( asset_server.load(Path::new("levels2d").join(&level)), )); }, ); app.add_plugins(bevy_yoleck::vpeol_2d::Vpeol2dPluginForGame); } else { app.add_plugins(EguiPlugin); app.add_plugins(YoleckPluginForEditor); // Adding `YoleckEditorLevelsDirectoryPath` is not usually required - // `YoleckPluginForEditor` will add one with "assets/levels". Here we want to support // example2d and example3d in the same repository so we use different directories. app.insert_resource(bevy_yoleck::YoleckEditorLevelsDirectoryPath( Path::new(".").join("assets").join("levels2d"), )); app.add_plugins(Vpeol2dPluginForEditor); app.add_plugins(VpeolSelectionCuePlugin::default()); #[cfg(target_arch = "wasm32")] app.add_systems( Startup, |asset_server: Res, mut commands: Commands| { commands.spawn(YoleckLoadLevel(asset_server.load("levels2d/example.yol"))); }, ); } app.add_systems(Startup, |world: &mut World| { world.init_resource::(); }); app.add_plugins(YoleckEntityUpgradingPlugin { app_format_version: 1, }); app.add_systems(Startup, setup_camera); app.add_yoleck_entity_type({ YoleckEntityType::new("Player") .with::() .with::() .insert_on_init(|| IsPlayer) }); app.add_yoleck_edit_system(edit_player); app.add_systems(YoleckSchedule::Populate, populate_player); app.add_yoleck_entity_upgrade_for(1, "Player", |data| { let mut old_data = data.as_object_mut().unwrap().remove("Player").unwrap(); data["Vpeol2dPosition"] = old_data.get_mut("position").unwrap().take(); }); app.add_yoleck_entity_type({ YoleckEntityType::new("Fruit") .with_uuid() .with::() .with::() }); app.add_yoleck_edit_system(duplicate_fruit); app.add_yoleck_edit_system(edit_fruit_type); app.add_systems(YoleckSchedule::Populate, populate_fruit); app.add_yoleck_entity_upgrade(1, |type_name, data| { if type_name != "Fruit" { return; } let mut old_data = data.as_object_mut().unwrap().remove("Fruit").unwrap(); data["Vpeol2dPosition"] = old_data.get_mut("position").unwrap().take(); data["FruitType"] = serde_json::json!({ "index": old_data.get_mut("fruit_index").unwrap().take(), }); }); app.add_yoleck_entity_type({ YoleckEntityType::new("FloatingText") .with::() .with::() .with::() .with::() }); app.add_yoleck_edit_system(edit_text); app.add_systems(YoleckSchedule::Populate, populate_text); app.add_yoleck_entity_upgrade(1, |type_name, data| { if type_name != "FloatingText" { return; } let mut old_data = data .as_object_mut() .unwrap() .remove("FloatingText") .unwrap(); data["Vpeol2dPosition"] = old_data.get_mut("position").unwrap().take(); data["TextContent"] = serde_json::json!({ "text": old_data.get_mut("text").unwrap().take(), }); data["Vpeol2dScale"] = serde_json::to_value( Vec2::ONE * old_data.get_mut("scale").unwrap().take().as_f64().unwrap() as f32, ) .unwrap(); }); app.add_yoleck_entity_type({ YoleckEntityType::new("Triangle") .with::() .with::() }); app.add_yoleck_edit_system(edit_triangle); app.add_systems(YoleckSchedule::Populate, populate_triangle); app.add_yoleck_edit_system(edit_laser_pointer); app.add_systems(Update, draw_laser_pointers); app.add_systems( Update, (control_player, eat_fruits).run_if(in_state(YoleckEditorState::GameActive)), ); app.run(); } fn setup_camera(mut commands: Commands) { let mut camera = Camera2dBundle::default(); camera.transform.translation.z = 100.0; commands .spawn(camera) .insert(VpeolCameraState::default()) .insert(Vpeol2dCameraControl::default()); } #[derive(Resource)] struct GameAssets { fruits_sprite_sheet_texture: Handle, fruits_sprite_sheet_layout: Handle, fruits_sprite_sheet_egui: (egui::TextureId, Vec), font: Handle, } impl FromWorld for GameAssets { fn from_world(world: &mut World) -> Self { let mut system_state = SystemState::<( Res, ResMut>, EguiContexts, )>::new(world); let (asset_server, mut texture_atlas_layout_assets, mut egui_context) = system_state.get_mut(world); let fruits_atlas_texture = asset_server.load("sprites/fruits.png"); let fruits_atlas_layout = TextureAtlasLayout::from_grid(UVec2::new(64, 64), 3, 1, None, None); let fruits_egui = { ( egui_context.add_image(fruits_atlas_texture.clone()), fruits_atlas_layout .textures .iter() .map(|rect| { [ [ rect.min.x as f32 / fruits_atlas_layout.size.x as f32, rect.min.y as f32 / fruits_atlas_layout.size.y as f32, ] .into(), [ rect.max.x as f32 / fruits_atlas_layout.size.x as f32, rect.max.y as f32 / fruits_atlas_layout.size.y as f32, ] .into(), ] .into() }) .collect(), ) }; Self { fruits_sprite_sheet_texture: fruits_atlas_texture, fruits_sprite_sheet_layout: texture_atlas_layout_assets.add(fruits_atlas_layout), fruits_sprite_sheet_egui: fruits_egui, font: asset_server.load("fonts/FiraSans-Bold.ttf"), } } } #[derive(Component)] struct IsPlayer; #[derive(Clone, PartialEq, Serialize, Deserialize)] struct Player { #[serde(default)] position: Vec2, #[serde(default)] rotation: f32, } fn populate_player( mut populate: YoleckPopulate<(), With>, asset_server: Res, mut texture_cache: Local>>, ) { populate.populate(|_ctx, mut cmd, ()| { cmd.insert((SpriteBundle { sprite: Sprite { custom_size: Some(Vec2::new(100.0, 100.0)), ..Default::default() }, texture: texture_cache .get_or_insert_with(|| asset_server.load("sprites/player.png")) .clone(), ..Default::default() },)); }); } fn edit_player( mut ui: ResMut, mut edit: YoleckEdit<(&IsPlayer, &Vpeol2dPosition, &mut Vpeol2dRotatation)>, mut knobs: YoleckKnobs, ) { let Ok((_, Vpeol2dPosition(position), mut rotation)) = edit.get_single_mut() else { return; }; use std::f32::consts::PI; ui.add(egui::Slider::new(&mut rotation.0, PI..=-PI).prefix("Angle: ")); // TODO: do this in vpeol_2d? let mut rotate_knob = knobs.knob("rotate"); let knob_position = position.extend(1.0) + Quat::from_rotation_z(rotation.0) * (50.0 * Vec3::Y); rotate_knob.cmd.insert(SpriteBundle { sprite: Sprite { color: css::PURPLE.into(), custom_size: Some(Vec2::new(30.0, 30.0)), ..Default::default() }, transform: Transform::from_translation(knob_position), global_transform: Transform::from_translation(knob_position).into(), ..Default::default() }); if let Some(rotate_to) = rotate_knob.get_passed_data::() { rotation.0 = Vec2::Y.angle_between(rotate_to.truncate() - *position); } } fn control_player( mut player_query: Query<&mut Transform, With>, time: Res