diff --git a/Cargo.lock b/Cargo.lock index de93e3e..488ad27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,15 @@ dependencies = [ "glam", ] +[[package]] +name = "bevy_pancam" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ebc698de3f4e824a67f517fe9cca35f08d5fdaab99ed8e141102c582768fc62" +dependencies = [ + "bevy", +] + [[package]] name = "bevy_pbr" version = "0.8.1" @@ -2645,6 +2654,7 @@ name = "worlds-sim-rust" version = "0.1.0" dependencies = [ "bevy", + "bevy_pancam", "save", ] diff --git a/Cargo.toml b/Cargo.toml index 30a5c04..8ee5be5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" [features] debug = ["save/debug"] -render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/render", "save/render"] +render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/render", "save/render", "dep:bevy_pancam"] default = ["render", "debug"] [dependencies.save] @@ -15,3 +15,7 @@ path = "save" [dependencies.bevy] version = "0.8" default-features = false + +[dependencies.bevy_pancam] +version = "0.6.1" +optional = true \ No newline at end of file diff --git a/save/src/world.rs b/save/src/world.rs index 4b31c3c..ea3035c 100644 --- a/save/src/world.rs +++ b/save/src/world.rs @@ -4,7 +4,9 @@ use std::{ fmt::{Debug, Display}, }; -use bevy::{math::Vec3A, prelude::Vec2, utils::default}; +// TODO: Logging doesn't seem to work here? Figure out why and fix + +use bevy::{log::info, math::Vec3A, prelude::Vec2, utils::default}; use noise::{NoiseFn, Perlin, Seedable}; use rand::Rng; @@ -145,11 +147,14 @@ impl World { } fn generate_continents(&mut self) { + info!("Generating continents"); let mut rng = rand::thread_rng(); let width = self.width as f32; let height = self.height as f32; for i in 0..Self::NUM_CONTINENTS { + info!("{}/{}", i, Self::NUM_CONTINENTS); + self.continent_offsets[i as usize].x = rng .gen_range(width * i as f32 * 2.0 / 5.0..width * (i as f32 + 2.0) * 2.0 / 5.0) .repeat(width); @@ -159,6 +164,7 @@ impl World { self.continent_widths[i as usize] = rng.gen_range(Self::CONTINENT_MIN_WIDTH_FACTOR..Self::CONTINENT_MAX_WIDTH_FACTOR); } + info!("Done generating continents"); } fn continent_modifier(&self, x: usize, y: usize) -> f32 { @@ -195,6 +201,7 @@ impl World { } fn generate_altitude(&mut self) -> Result<(), CartesianError> { + info!("Generating altitude"); self.generate_continents(); const RADIUS_1: f32 = 0.5; @@ -212,6 +219,8 @@ impl World { for y in 0..self.terrain.len() { let alpha = (y as f32 / self.height as f32) * PI; + info!("{}/{}", y, self.terrain.len()); + for x in 0..self.terrain[y].len() { let beta = (x as f32 / self.width as f32) * TAU; @@ -250,6 +259,7 @@ impl World { self.terrain[y][x].altitude = Self::calculate_altitude(raw_altitude); } } + info!("Done generating altitude"); Ok(()) } diff --git a/save/src/world_manager.rs b/save/src/world_manager.rs index 5982af5..c56502c 100644 --- a/save/src/world_manager.rs +++ b/save/src/world_manager.rs @@ -67,6 +67,9 @@ impl WorldManager { pub fn get_world(&self) -> Option<&World> { self.world.as_ref() } + pub fn world(&self) -> &World { + self.get_world().unwrap() + } pub fn new_world(&mut self) -> Result<&World, WorldGenError> { let seed = random(); @@ -142,7 +145,7 @@ impl WorldManager { let mut shade_value = 1.0; while shade_value > altitude / World::MAX_ALTITUDE { - shade_value -= 0.1; + shade_value -= 0.05; } Color::rgb(shade_value, shade_value, shade_value) diff --git a/src/main.rs b/src/main.rs index 08b7e43..f1ed5b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,9 @@ #![warn(unused_results)] #![warn(variant_size_differences)] -mod world_plugins; +mod plugins; + +use std::fmt::Display; use bevy::{ app::App, @@ -42,7 +44,7 @@ use bevy::{ #[cfg(feature = "render")] use bevy::{ asset::{AssetServer, Assets}, - core_pipeline::core_2d::Camera2dBundle, + core_pipeline::core_2d::{Camera2d, Camera2dBundle}, ecs::{ change_detection::ResMut, component::Component, @@ -50,24 +52,31 @@ use bevy::{ system::{Commands, Query, Res}, }, hierarchy::BuildChildren, + prelude::Vec2, render::{ + camera::{Camera, RenderTarget}, color::Color, render_resource::{ Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, }, texture::{Image, ImageSettings}, }, + sprite::{Sprite, SpriteBundle}, + text::Text, + transform::components::GlobalTransform, ui::{ - entity::{ButtonBundle, ImageBundle, NodeBundle, TextBundle}, + entity::{ButtonBundle, NodeBundle, TextBundle}, widget::Button, AlignItems, FocusPolicy, Interaction, JustifyContent, PositionType, Size, Style, UiColor, - UiImage, UiRect, Val, + UiRect, Val, }, utils::default, window::{CursorIcon, WindowDescriptor, Windows}, winit::WinitSettings, }; -use world_plugins::WorldPlugins; +#[cfg(feature = "render")] +use plugins::PanCam; +use plugins::WorldPlugins; use save::*; #[cfg(feature = "render")] @@ -78,17 +87,33 @@ fn refresh_world_texture(images: &mut Assets, world_manager: &WorldManage } #[cfg(feature = "render")] -#[derive(Component, Default)] +#[derive(Component)] struct RainfallButton; #[cfg(feature = "render")] -#[derive(Component, Default)] +#[derive(Component)] struct TemperatureButton; #[cfg(feature = "render")] -#[derive(Component, Default)] +#[derive(Component)] struct ContoursButton; +#[cfg(feature = "render")] +#[derive(Component)] +struct InfoPanel; + +#[cfg(feature = "render")] +#[derive(Default, Debug)] +struct CursorMapPosition { + x: i32, + y: i32, +} +impl Display for CursorMapPosition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("x: {}, y: {}", self.x, self.y)) + } +} + const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.60, 0.35); @@ -191,6 +216,59 @@ fn handle_contours_button( } } +#[cfg(feature = "render")] +fn update_cursor_map_position( + mut cursor_map_position: ResMut<'_, CursorMapPosition>, + transform: Query<'_, '_, (&Camera, &GlobalTransform), With>, + windows: Res<'_, Windows>, + world_manager: Res<'_, WorldManager>, +) { + let (camera, transform) = transform.single(); + + let window = match camera.target { + RenderTarget::Window(window_id) => windows.get(window_id).unwrap(), + RenderTarget::Image(_) => windows.primary(), + }; + + if let Some(screen_position) = window.cursor_position() { + let window_size = Vec2::new(window.width(), window.height()); + + // GPU coordinates [-1..1] + let ndc = (screen_position / window_size) * 2.0; + + // Matrix to reverse camera transform + let ndc_to_world = transform.compute_matrix() * camera.projection_matrix().inverse(); + + let world_position = + ndc_to_world.project_point3(ndc.extend(-1.0)).truncate() / WORLD_SCALE as f32; + + cursor_map_position.x = world_position.x.round() as i32; + cursor_map_position.y = world_manager.world().height - world_position.y.round() as i32; + } +} + +#[cfg(feature = "render")] +fn update_info_panel( + cursor_position: Res<'_, CursorMapPosition>, + world_manager: Res<'_, WorldManager>, + mut text: Query<'_, '_, &mut Text, With>, +) { + let world = world_manager.world(); + text.single_mut().sections[0].value = if cursor_position.x >= 0 + && cursor_position.x < world.width + && cursor_position.y >= 0 + && cursor_position.y < world.height + { + let cell = &world.terrain[cursor_position.y as usize][cursor_position.x as usize]; + format!( + "Mouse position: {}\nAltitude: {}\nRainfall: {}\nTemperature: {}", + *cursor_position, cell.altitude, cell.rainfall, cell.temperature + ) + } else { + format!("Mouse position: {}\nOut of bounds", *cursor_position) + }; +} + #[cfg(feature = "render")] fn generate_graphics( mut commands: Commands<'_, '_>, @@ -198,7 +276,13 @@ fn generate_graphics( mut world_manager: ResMut<'_, WorldManager>, asset_server: Res<'_, AssetServer>, ) { - let world = world_manager.get_world().unwrap(); + use bevy::ui::AlignSelf; + + let world = world_manager.world(); + let custom_sprite_size = Vec2 { + x: (WORLD_SCALE * world.width) as f32, + y: (WORLD_SCALE * world.height) as f32, + }; let image_handle = images.add(Image { data: world_manager.world_color_bytes(), @@ -219,7 +303,17 @@ fn generate_graphics( }); world_manager.image_handle_id = image_handle.id; - _ = commands.spawn_bundle(Camera2dBundle::default()); + _ = commands + .spawn_bundle(Camera2dBundle::default()) + .insert(PanCam::default()); + _ = commands.spawn_bundle(SpriteBundle { + texture: images.get_handle(world_manager.image_handle_id), + sprite: Sprite { + custom_size: Some(custom_sprite_size), + ..default() + }, + ..default() + }); _ = commands .spawn_bundle(NodeBundle { style: Style { @@ -230,15 +324,33 @@ fn generate_graphics( ..default() }) .with_children(|root_node| { - _ = root_node.spawn_bundle(ImageBundle { - style: Style { - size: Size::new(Val::Percent(100.0), Val::Auto), + _ = root_node + .spawn_bundle(NodeBundle { + style: Style { + // align_items: AlignItems::FlexEnd, + align_self: AlignSelf::FlexEnd, + padding: UiRect::all(Val::Px(2.0)), + ..default() + }, + color: Color::rgba(1.0, 1.0, 1.0, 0.05).into(), + focus_policy: FocusPolicy::Pass, ..default() - }, - image: UiImage(image_handle), - ..default() - }); - + }) + .with_children(|info_panel| { + _ = info_panel + .spawn_bundle(TextBundle { + text: Text::from_section( + "Info Panel", + bevy::text::TextStyle { + font: asset_server.load("JuliaMono.ttf"), + font_size: 15.0, + color: Color::WHITE, + }, + ), + ..default() + }) + .insert(InfoPanel); + }); _ = root_node .spawn_bundle(NodeBundle { style: Style { @@ -264,7 +376,7 @@ fn generate_graphics( color: NORMAL_BUTTON.into(), ..default() }) - .insert(RainfallButton::default()) + .insert(RainfallButton) .with_children(|button| { _ = button.spawn_bundle(TextBundle { text: bevy::text::Text::from_section( @@ -289,7 +401,7 @@ fn generate_graphics( color: NORMAL_BUTTON.into(), ..default() }) - .insert(TemperatureButton::default()) + .insert(TemperatureButton) .with_children(|button| { _ = button.spawn_bundle(TextBundle { text: bevy::text::Text::from_section( @@ -314,7 +426,7 @@ fn generate_graphics( color: NORMAL_BUTTON.into(), ..default() }) - .insert(ContoursButton::default()) + .insert(ContoursButton) .with_children(|button| { _ = button.spawn_bundle(TextBundle { text: bevy::text::Text::from_section( @@ -332,6 +444,7 @@ fn generate_graphics( }); } +const WORLD_SCALE: i32 = 3; fn main() -> Result<(), Box> { let mut app = App::new(); let mut manager = WorldManager::new(); @@ -344,16 +457,19 @@ fn main() -> Result<(), Box> { // Use nearest-neighbor rendering for cripsier pixels .insert_resource(ImageSettings::default_nearest()) .insert_resource(WindowDescriptor { - width: (2 * world.width) as f32, - height: (2 * world.height) as f32, + width: (WORLD_SCALE * world.width) as f32, + height: (WORLD_SCALE * world.height) as f32, title: String::from("World-RS"), resizable: true, ..default() }) + .insert_resource(CursorMapPosition::default()) .add_startup_system(generate_graphics) .add_system(handle_rainfall_button) .add_system(handle_temperature_button) - .add_system(handle_contours_button); + .add_system(handle_contours_button) + .add_system(update_cursor_map_position) + .add_system(update_info_panel); } #[cfg(not(feature = "render"))] { @@ -375,7 +491,7 @@ fn main() -> Result<(), Box> { }); } - app.insert_resource(manager).add_plugins(WorldPlugins).run(); + app.add_plugins(WorldPlugins).insert_resource(manager).run(); Ok(()) } diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs new file mode 100644 index 0000000..bd6d6bf --- /dev/null +++ b/src/plugins/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod world_plugins; +pub(crate) use world_plugins::WorldPlugins; + +pub(crate) use bevy_pancam::PanCam; diff --git a/src/world_plugins.rs b/src/plugins/world_plugins.rs similarity index 92% rename from src/world_plugins.rs rename to src/plugins/world_plugins.rs index 61c5bcd..2add5b6 100644 --- a/src/world_plugins.rs +++ b/src/plugins/world_plugins.rs @@ -22,6 +22,8 @@ impl PluginGroup for WorldPlugins { input::InputPlugin, render::RenderPlugin, sprite::SpritePlugin, text::TextPlugin, transform::TransformPlugin, ui::UiPlugin, window::WindowPlugin, winit::WinitPlugin, }; + use bevy_pancam::PanCamPlugin; + _ = group .add(TransformPlugin::default()) // hierarchy @@ -34,7 +36,8 @@ impl PluginGroup for WorldPlugins { .add(CorePipelinePlugin::default()) .add(SpritePlugin::default()) .add(TextPlugin::default()) - .add(UiPlugin::default()); + .add(UiPlugin::default()) + .add(PanCamPlugin::default()); } #[cfg(not(feature = "render"))] {