diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..1aa838e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["-Z", "threads=8"] diff --git a/Cargo.lock b/Cargo.lock index 89e6897..c0d82c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.12", "once_cell", "version_check", "zerocopy", @@ -1136,7 +1136,7 @@ checksum = "7915222f4a08ccc782e08d10b751b42e5f9d786e697d0cb3fd09333cb7e8b6ea" dependencies = [ "ahash", "bevy_utils_proc_macros", - "getrandom", + "getrandom 0.2.12", "hashbrown 0.14.3", "instant", "nonmax", @@ -1326,6 +1326,8 @@ version = "0.1.0" dependencies = [ "bevy", "bevy_egui", + "noise", + "paste", ] [[package]] @@ -2115,6 +2117,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -2124,7 +2137,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2854,7 +2867,7 @@ checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2968,6 +2981,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "noise" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba869e17168793186c10ca82c7079a4ffdeac4f1a7d9e755b9491c028180e40" +dependencies = [ + "num-traits", + "rand 0.7.3", + "rand_xorshift", +] + [[package]] name = "nom" version = "7.1.3" @@ -3410,6 +3434,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17fd96390ed3feda12e1dfe2645ed587e0bea749e319333f104a33ff62f77a0b" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -3417,8 +3454,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -3428,7 +3475,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -3437,7 +3493,25 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -3542,7 +3616,7 @@ dependencies = [ "home", "log", "mio", - "rand", + "rand 0.8.5", "tungstenite", "uuid", ] @@ -3574,7 +3648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", - "getrandom", + "getrandom 0.2.12", "libc", "spin", "untrusted", @@ -4277,7 +4351,7 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.8.5", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -4365,7 +4439,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ - "getrandom", + "getrandom 0.2.12", "serde", ] @@ -4403,6 +4477,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/README.md b/README.md index e90e662..38e5d2f 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Our team consists of : - [CoCo_Sol](https://github.com/cocosol007): Owner of this project and the main developper - [Raphaƫl](https://git.tipragot.fr/raphael): Owner of this project and game designer - [Tipragot](https://git.tipragot.fr/tipragot): Main reviewer -- [Arthur](https://www.instagram.com/tuturdu32): Compositeur +- [Arthur](https://www.instagram.com/tuturdu32): Composer - [Racloody](https://linktr.ee/racloody): Sound designer and chief communications officer - [Horoleysh](https://instagram.com/horoleysh): Artist diff --git a/crates/border-wars/Cargo.toml b/crates/border-wars/Cargo.toml index 2a58d57..b7d0560 100644 --- a/crates/border-wars/Cargo.toml +++ b/crates/border-wars/Cargo.toml @@ -12,4 +12,6 @@ workspace = true [dependencies] bevy = { version = "0.12.1", features = ["dynamic_linking"] } -bevy_egui = "0.24.0" \ No newline at end of file +bevy_egui = "0.24.0" +noise = "0.8.2" +paste = "1.0.14" diff --git a/crates/border-wars/assets/tiles/forest.png b/crates/border-wars/assets/tiles/forest.png new file mode 100644 index 0000000..1e4d2d5 Binary files /dev/null and b/crates/border-wars/assets/tiles/forest.png differ diff --git a/crates/border-wars/assets/tiles/grass.png b/crates/border-wars/assets/tiles/grass.png new file mode 100644 index 0000000..f7327e7 Binary files /dev/null and b/crates/border-wars/assets/tiles/grass.png differ diff --git a/crates/border-wars/assets/tiles/hill.png b/crates/border-wars/assets/tiles/hill.png new file mode 100644 index 0000000..e8cabed Binary files /dev/null and b/crates/border-wars/assets/tiles/hill.png differ diff --git a/crates/border-wars/src/camera.rs b/crates/border-wars/src/camera.rs new file mode 100644 index 0000000..6c2b50b --- /dev/null +++ b/crates/border-wars/src/camera.rs @@ -0,0 +1,123 @@ +//! This module contains the camera systems responsible for movement and +//! scaling. + +use bevy::input::mouse::{MouseScrollUnit, MouseWheel}; +use bevy::prelude::*; + +use crate::CurrentScene; + +/// The speed of camera movement. +#[derive(Resource)] +struct CameraSpeedMouvement(f32); + +/// The speed of camera scaling. +#[derive(Resource)] +struct CameraSpeedScale(f32); + +/// The minimum scale of the camera. +#[derive(Resource)] +struct MinimumScale(f32); + +/// The maximum scale of the camera. +#[derive(Resource)] +struct MaximumScale(f32); + +/// Key settings for camera movement. +#[derive(Resource)] +pub struct KeysMovementSettings { + /// Key to move the camera up. + pub up: KeyCode, + + /// Key to move the camera down. + pub down: KeyCode, + + /// Key to move the camera right. + pub right: KeyCode, + + /// Key to move the camera left. + pub left: KeyCode, +} + +/// A Bevy plugin for the camera. +/// Allows camera movement with the keyboard and scaling with the mouse. +pub struct CameraPlugin; + +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, init_camera) + .add_systems(Startup, init_resources_for_camera) + .add_systems(Update, movement_system.run_if(in_state(CurrentScene::Game))) + .add_systems(Update, scale_system.run_if(in_state(CurrentScene::Game))); + } +} + +/// Initializes the camera. +fn init_camera(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); +} + +/// Initializes the resources related to the camera. +/// +/// - [KeysMovementSettings]: The key settings for camera movement. +/// - [CameraSpeedMouvement]: The speed of camera movement. +/// - [CameraSpeedScale]: The speed of camera scaling. +/// - [MinimumScale]: The minimum scale of the camera. +/// - [MaximumScale]: The maximum scale of the camera. +fn init_resources_for_camera(mut commands: Commands) { + commands.insert_resource(KeysMovementSettings { + up: KeyCode::Z, + down: KeyCode::S, + right: KeyCode::D, + left: KeyCode::Q, + }); + + commands.insert_resource(CameraSpeedMouvement(10.0)); + commands.insert_resource(CameraSpeedScale(0.1)); + commands.insert_resource(MinimumScale(0.1)); + commands.insert_resource(MaximumScale(10.0)); +} + +/// Moves the camera with keyboard input. +fn movement_system( + mut query: Query<&mut Transform, With>, + keys: Res>, + keys_settings: Res, + movement_speed: Res, +) { + for mut transform in query.iter_mut() { + let mut target = Vec3::ZERO; + for key in keys.get_pressed() { + match *key { + value if value == keys_settings.up => target.y += movement_speed.0, + value if value == keys_settings.down => target.y -= movement_speed.0, + value if value == keys_settings.right => target.x += movement_speed.0, + value if value == keys_settings.left => target.x -= movement_speed.0, + _ => continue, + } + } + + transform.translation += target; + } +} + +/// Scales the view with mouse input. +fn scale_system( + mut scroll_event: EventReader, + mut query: Query<&mut OrthographicProjection, With>, + min_scale: Res, + max_scale: Res, + scale_speed: Res, +) { + for event in scroll_event.read() { + for mut projection in query.iter_mut() { + if event.unit != MouseScrollUnit::Line { + return; + } + + let future_scale = event.y.mul_add(-scale_speed.0, projection.scale); + if min_scale.0 < future_scale && future_scale < max_scale.0 { + projection.scale = future_scale; + } + } + } +} diff --git a/crates/border-wars/src/lib.rs b/crates/border-wars/src/lib.rs index 79c8ece..a971021 100644 --- a/crates/border-wars/src/lib.rs +++ b/crates/border-wars/src/lib.rs @@ -2,6 +2,9 @@ use bevy::prelude::*; +pub mod camera; +pub mod map; +pub mod responsive_scale; pub mod scenes; /// The current scene of the game. diff --git a/crates/border-wars/src/main.rs b/crates/border-wars/src/main.rs index 69d6a9a..15d9cee 100644 --- a/crates/border-wars/src/main.rs +++ b/crates/border-wars/src/main.rs @@ -1,6 +1,10 @@ //! The main entry point of the game. -use bevy::{prelude::*, text::TextSettings}; +use bevy::prelude::*; +use bevy::text::TextSettings; +use border_wars::camera::CameraPlugin; +use border_wars::map::click_tile::TilesClickable; +use border_wars::map::renderer::RendererPlugin; use border_wars::scenes::ScenesPlugin; fn main() { @@ -11,5 +15,8 @@ fn main() { }) .add_plugins(DefaultPlugins) .add_plugins(ScenesPlugin) + .add_plugins(RendererPlugin) + .add_plugins(CameraPlugin) + .add_plugins(TilesClickable) .run(); } diff --git a/crates/border-wars/src/map/click_tile.rs b/crates/border-wars/src/map/click_tile.rs new file mode 100644 index 0000000..fb66845 --- /dev/null +++ b/crates/border-wars/src/map/click_tile.rs @@ -0,0 +1,105 @@ +//! All programs related to the clicking on a tile. + +use bevy::prelude::*; + +use super::Tile; + +/// The event that is triggered when a tile is clicked. +/// +/// The event contains the index (ID) of the clicked tile. +#[derive(Event)] +pub struct TileJustClicked(pub u32); + +/// An event that is triggered when a mouse button is clicked. +/// +/// The event contains the position of the cursor in the world. +#[derive(Event)] +struct ClickOnTheWorld(Vec2); + +/// A zone that can't be clicked. +/// For exemple the UI of the game. +#[derive(Component)] +pub struct ZoneNotClickable; + +/// A plugin that handles the selection of tiles. +pub struct TilesClickable; + +impl Plugin for TilesClickable { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, mouse_handler) + .add_systems(PreUpdate, select_closest_tile) + .add_event::() + .add_event::(); + } +} + +/// Handles the mouse click and gets the position of the cursor in the world. +/// Finally, it sends an event with the position of the cursor. +fn mouse_handler( + mouse_button_input: Res>, + windows: Query<&Window>, + cameras: Query<(&Camera, &GlobalTransform)>, + mut events_writer: EventWriter, + not_clickable_zones: Query<(&Node, &GlobalTransform), With>, + ui_scale: Res, +) { + if !mouse_button_input.just_pressed(MouseButton::Left) { + return; + } + + let window = windows.get_single().expect("Main window not found"); + + let cursor_position_on_screen = window.cursor_position(); + + let Some(cursor_position_on_screen) = cursor_position_on_screen else { + return; + }; + + for (node, global_transform) in not_clickable_zones.iter() { + let rect = node.physical_rect(global_transform, window.scale_factor(), ui_scale.0); + if rect.contains(cursor_position_on_screen) { + return; + } + } + + let (camera, camera_transform) = cameras.get_single().expect("Camera not found"); + + let cursor_position_in_world = camera + .viewport_to_world(camera_transform, cursor_position_on_screen) + .expect("Failed to convert cursor position") + .origin + .truncate(); + + events_writer.send(ClickOnTheWorld(cursor_position_in_world)); +} + +/// Get the closest tile to the cursor and send it in an event. +fn select_closest_tile( + tiles: Query<(Entity, &Transform, &Tile)>, + mut click_event_reader: EventReader, + mut clicked_tile_event_writer: EventWriter, +) { + for click_event in click_event_reader.read() { + // The closest tile and its distance to the cursor. + let mut closest_entity: Option = None; + let mut closest_position: Option = None; + + for (tile_entity, tile_transform, tile_type) in tiles.iter() { + let mut tile_position = tile_transform.translation.truncate(); + let tile_size = tile_type.get_image_size(); + let tile_scale = tile_transform.scale.truncate(); + + tile_position += (tile_size / 2.0) * tile_scale; + + let distance_to_cursor = tile_position.distance(click_event.0); + + if closest_position.is_none() || closest_position > Some(distance_to_cursor) { + closest_entity = Some(tile_entity); + closest_position = Some(distance_to_cursor); + } + } + if let Some(tile_entity) = closest_entity { + clicked_tile_event_writer.send(TileJustClicked(tile_entity.index())); + } + } +} diff --git a/crates/border-wars/src/map/generation.rs b/crates/border-wars/src/map/generation.rs new file mode 100644 index 0000000..f1ce9ee --- /dev/null +++ b/crates/border-wars/src/map/generation.rs @@ -0,0 +1,98 @@ +//! All functions related to the generation of the map. + +use bevy::prelude::*; +use noise::{NoiseFn, Perlin}; + +use super::hex::*; +use super::{Tile, TilePosition}; + +/// A plugin to handle the map generation. +pub struct MapGenerationPlugin; + +/// The zoom of the map during the generation. +const MAP_GENERATION_SCALE: f32 = 5.; + +impl Plugin for MapGenerationPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_systems( + Update, + (delete_map, generate_map.after(delete_map)) + .run_if(in_state(crate::CurrentScene::Game)), + ); + } +} + +/// An event to trigger the generation of the map. +#[derive(Event)] +pub struct StartMapGeneration { + /// The seed used to generate the map. + pub seed: u32, + + /// The radius of the map. + pub radius: u16, +} + +/// An event send when the map is generated. +#[derive(Event)] +pub struct EndMapGeneration; + +/// Generate each tiles of the map if the [StartMapGeneration] is received. +/// +/// The map is generated using a [Perlin] noise and a [HexSpiral]. +/// +/// It's generated one tile at a time, until the spiral is finished. +fn generate_map( + mut start_generation_events: EventReader, + mut end_generation_writer: EventWriter, + mut commands: Commands, + mut local_noise: Local>, + mut local_spiral: Local>>, +) { + // Handle map generation events and create the spiral and the noise. + for event in start_generation_events.read() { + *local_noise = Some(Perlin::new(event.seed)); + *local_spiral = Some(TilePosition::new(0, 0).spiral(event.radius as usize)); + } + + // Check if the map is being generated. + let (Some(noise), Some(spiral)) = (local_noise.as_ref(), local_spiral.as_mut()) else { + return; + }; + + // Spawn a tile until the spiral is finished + // If the map is generated, we send [EndMapGeneration] and set the local + // variables to None. + if let Some(position) = spiral.next() { + commands.spawn((get_tile_type(position, noise), position as TilePosition)); + } else { + end_generation_writer.send(EndMapGeneration); + *local_noise = None; + *local_spiral = None; + } +} + +/// Returns the type of the [HexPosition] with the given noise. +fn get_tile_type(position: HexPosition, noise: &Perlin) -> Tile { + let pixel_position = position.to_pixel_coordinates() / MAP_GENERATION_SCALE; + let value = noise.get([pixel_position.x as f64, pixel_position.y as f64]); + match value { + v if v <= -0.4 => Tile::Hill, + v if v >= 0.4 => Tile::Forest, + _ => Tile::Grass, + } +} + +/// Despawns the tiles if the event [StartMapGeneration] is received. +fn delete_map( + mut commands: Commands, + query: Query>, + mut start_generation_events: EventReader, +) { + for _ in start_generation_events.read() { + for entity in query.iter() { + commands.entity(entity).despawn_recursive(); + } + } +} diff --git a/crates/border-wars/src/map/hex.rs b/crates/border-wars/src/map/hex.rs new file mode 100644 index 0000000..415bdfc --- /dev/null +++ b/crates/border-wars/src/map/hex.rs @@ -0,0 +1,383 @@ +//! All functions related to calculations in a hexagonal grid. + +use std::ops::{ + Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, +}; + +use bevy::prelude::*; +use paste::paste; + +/// Represents a number that can be used in calculations for hexagonal grids. +pub trait Number: + Copy + + PartialEq + + PartialOrd + + Add + + Sub + + Mul + + Div + + Rem + + Neg + + AddAssign + + SubAssign + + MulAssign + + DivAssign + + RemAssign + + std::fmt::Debug +{ + /// The number -2. + const MINUS_TWO: Self; + + /// The number -1. + const MINUS_ONE: Self; + + /// The number 0. + const ZERO: Self; + + /// The number 1. + const ONE: Self; + + /// The number 2. + const TWO: Self; + + /// Returns the maximum of `self` and `other`. + fn max(self, other: Self) -> Self { + if self > other { self } else { other } + } + + /// Returns the minimum of `self` and `other`. + fn min(self, other: Self) -> Self { + if self < other { self } else { other } + } + + /// Returns the absolute value of `self`. + fn abs(self) -> Self { + if self < Self::ZERO { -self } else { self } + } + + /// Converts an `usize` to `Self`. + fn from_usize(value: usize) -> Self; + + /// Converts `self` to an `f32`. + fn to_f32(self) -> f32; + + /// Converts an `f32` to `Self`. + fn from_f32(value: f32) -> Self; +} + +/// Implements the `Number` trait for the given types. +macro_rules! number_impl { + ($($t:ty,)*) => {paste!{$( + impl Number for $t { + const MINUS_ONE: Self = - [< 1 $t >]; + const MINUS_TWO: Self = - [< 2 $t >]; + const ZERO: Self = [< 0 $t >]; + const ONE: Self = [< 1 $t >]; + const TWO: Self = [< 2 $t >]; + + + fn from_usize(value: usize) -> Self { + value as $t + } + + fn to_f32(self) -> f32 { + self as f32 + } + + fn from_f32(value: f32) -> Self { + value as $t + } + } + )*}}; +} + +number_impl! { + i8, i16, i32, i64, i128, isize, + f32, f64, +} + +/// Represents a position in a hexagonal grid. +/// We use the axial coordinate system explained in this +/// [documentation](https://www.redblobgames.com/grids/hexagons/#coordinates). +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Component)] +pub struct HexPosition(pub T, pub T); + +/// All possible directions in a hexagonal grid. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum HexDirection { + /// The direction right. + Right, + + /// The direction up-right. + UpRight, + + /// The direction up-left. + UpLeft, + + /// The direction left. + Left, + + /// The direction down-left. + DownLeft, + + /// The direction down-right. + DownRight, +} + +impl HexDirection { + /// Returns the vector ([HexPosition]) of the direction. + /// + /// # Example + /// + /// ```no_run + /// use border_wars::map::hex::{HexDirection, HexPosition}; + /// + /// let direction = HexDirection::Right; + /// assert_eq!(direction.to_vector(), HexPosition(1, 0)); + /// ``` + pub const fn to_vector(self) -> HexPosition { + match self { + Self::Right => HexPosition(T::ONE, T::ZERO), + Self::UpRight => HexPosition(T::ONE, T::MINUS_ONE), + Self::UpLeft => HexPosition(T::ZERO, T::MINUS_ONE), + Self::Left => HexPosition(T::MINUS_ONE, T::ZERO), + Self::DownLeft => HexPosition(T::MINUS_ONE, T::ONE), + Self::DownRight => HexPosition(T::ZERO, T::ONE), + } + } +} + +/// A hexagonal ring iterator. +pub struct HexRing { + /// The current position in the ring. + current: HexPosition, + + /// The direction of the current position to the next in the ring. + direction: HexDirection, + + /// The radius of the ring. + radius: usize, + + /// The index of the current position in the ring. + index: usize, +} + +impl Iterator for HexRing { + type Item = HexPosition; + + fn next(&mut self) -> Option { + if self.index >= self.radius { + self.direction = match self.direction { + HexDirection::Right => HexDirection::UpRight, + HexDirection::UpRight => HexDirection::UpLeft, + HexDirection::UpLeft => HexDirection::Left, + HexDirection::Left => HexDirection::DownLeft, + HexDirection::DownLeft => HexDirection::DownRight, + HexDirection::DownRight => return None, + }; + self.index = 0; + } + let result = self.current; + self.current += self.direction.to_vector(); + self.index += 1; + Some(result) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = match self.direction { + HexDirection::Right => self.radius * 6, + HexDirection::UpRight => self.radius * 5, + HexDirection::UpLeft => self.radius * 4, + HexDirection::Left => self.radius * 3, + HexDirection::DownLeft => self.radius * 2, + HexDirection::DownRight => self.radius, + } - self.index; + (remaining, Some(remaining)) + } +} + +/// A hexagonal spiral iterator. +pub struct HexSpiral { + /// The origin of the spiral. + origin: HexPosition, + + /// The current ring of the spiral. + current: HexRing, + + /// The radius of the spiral. + radius: usize, + + /// The index of the current ring in the spiral. + index: usize, +} + +impl Iterator for HexSpiral { + type Item = HexPosition; + + fn next(&mut self) -> Option { + // The origin of the spiral. + if self.index == 0 { + self.index += 1; + return Some(self.origin); + } + if self.index > self.radius { + return None; + } + let mut result = self.current.next(); + if result.is_none() && self.index < self.radius { + self.index += 1; + self.current = self.origin.ring(self.index); + result = self.current.next(); + } + result + } +} + +impl HexPosition { + /// Creates a new [HexPosition]. + pub const fn new(x: T, y: T) -> Self { + Self(x, y) + } + + /// Converts the current [HexPosition] into a pixel coordinate. + /// + /// If you want to learn more about pixel coordinates conversion, + /// you can check the + /// [documentation](https://www.redblobgames.com/grids/hexagons/#hex-to-pixel). + /// + /// # Example + /// + /// ```no_run + /// use bevy::math::Vec2; + /// use border_wars::map::hex::HexPosition; + /// + /// let position = HexPosition(1, 0); + /// assert_eq!(position.to_pixel_coordinates(), (3f32.sqrt(), 0.0).into()); + /// ``` + pub fn to_pixel_coordinates(&self) -> Vec2 { + Vec2::new( + 3f32.sqrt() + .mul_add(T::to_f32(self.0), 3f32.sqrt() / 2.0 * T::to_f32(self.1)), + 3.0 / 2.0 * T::to_f32(self.1), + ) + } + + /// Returns the distance between two [HexPosition]s. + /// + /// # How it works + /// + /// In the hexagonal grid, using the + /// [cube coordinate system](https://www.redblobgames.com/grids/hexagons/#coordinates), + /// it's akin to a cube in 3D space. + /// The Manhattan distance between two positions is equal to half of + /// the sum of abs(dx) + abs(dy) + abs(dz). + /// However, in hexagonal grids, z is defined as -q - r. + /// + /// # Example + /// + /// ```no_run + /// use border_wars::map::hex::HexPosition; + /// + /// let a = HexPosition(0, 0); + /// let b = HexPosition(1, 1); + /// + /// assert_eq!(a.distance(b), 2); + /// ``` + pub fn distance(self, other: Self) -> T { + let Self(x, y) = self - other; + x.abs() + y.abs() + (x + y).abs() / T::TWO + } + + /// Returns the hexagonal ring of the given radius. + /// If you want to learn more about hexagonal grids, check the + /// [documentation](https://www.redblobgames.com/grids/hexagons/#rings) + /// + /// # Example + /// + /// ```no_run + /// use border_wars::map::hex::HexPosition; + /// + /// let position = HexPosition(0, 0); + /// let radius = 1; + /// + /// for ring_position in position.ring(radius) { + /// println!("{:?}", ring_position); + /// } + /// ``` + pub fn ring(self, radius: usize) -> HexRing { + HexRing { + current: self + HexDirection::DownLeft.to_vector() * T::from_usize(radius), + direction: HexDirection::Right, + radius, + index: 0, + } + } + + /// Returns the hexagonal spiral of the given radius. + /// If you want to learn more about hexagonal grids, check the + /// [documentation](https://www.redblobgames.com/grids/hexagons/#rings-spiral) + /// + /// # Example + /// + /// ```no_run + /// use border_wars::map::hex::HexPosition; + /// + /// let position = HexPosition(0, 0); + /// let radius = 1; + /// + /// for spiral_position in position.spiral(radius) { + /// println!("{:?}", spiral_position); + /// } + /// ``` + pub fn spiral(self, radius: usize) -> HexSpiral { + HexSpiral { + origin: self, + current: self.ring(1), + radius, + index: 0, + } + } +} + +/// Implementation of the arithmetic operators for hexagonal positions. +macro_rules! impl_ops { + ($(($t:ty, $n:ident),)*) => {paste!{$( + impl $t for HexPosition { + type Output = Self; + + fn $n(self, rhs: Self) -> Self { + Self(self.0.$n(rhs.0), self.1.$n(rhs.1)) + } + } + + impl $t for HexPosition { + type Output = Self; + + fn $n(self, rhs: T) -> Self { + Self(self.0.$n(rhs), self.1.$n(rhs)) + } + } + + impl [< $t Assign >] for HexPosition { + fn [< $n _assign >](&mut self, rhs: Self) { + self.0.[< $n _assign >](rhs.0) ; + self.1.[< $n _assign >](rhs.1) ; + } + } + + impl [< $t Assign >] for HexPosition { + fn [< $n _assign >](&mut self, rhs: T) { + self.0.[< $n _assign >](rhs); + self.1.[< $n _assign >](rhs); + } + } + )*}}; +} + +impl_ops! { + (Add, add), + (Sub, sub), + (Mul, mul), + (Div, div), + (Rem, rem), +} diff --git a/crates/border-wars/src/map/mod.rs b/crates/border-wars/src/map/mod.rs new file mode 100644 index 0000000..9172176 --- /dev/null +++ b/crates/border-wars/src/map/mod.rs @@ -0,0 +1,26 @@ +//! Contains all the logic related to the map. + +pub mod click_tile; +pub mod generation; +pub mod hex; +pub mod renderer; + +use bevy::prelude::*; + +use self::hex::*; + +/// The position of a tile in a hexagonal map. +pub type TilePosition = HexPosition; + +/// The tile of the map. +#[derive(Component, Debug)] +pub enum Tile { + /// The hill tile. + Hill, + + /// The grass tile. + Grass, + + /// The forest tile. + Forest, +} diff --git a/crates/border-wars/src/map/renderer.rs b/crates/border-wars/src/map/renderer.rs new file mode 100644 index 0000000..55fa90f --- /dev/null +++ b/crates/border-wars/src/map/renderer.rs @@ -0,0 +1,102 @@ +//! All functions related to the rendering of the map. + +use bevy::prelude::*; +use bevy::sprite::Anchor; + +use crate::map::{Tile, TilePosition}; + +/// A plugin to render the map. +pub struct RendererPlugin; + +impl Plugin for RendererPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, init_resources_for_rendering) + .add_systems( + Update, + render_map.run_if(in_state(crate::CurrentScene::Game)), + ) + .insert_resource(ClearColor(Color::rgb_u8(129, 212, 250))); + } +} + +/// The gap between the center of the tiles in the map. +#[derive(Resource)] +struct TilesGap(Vec2); + +/// The size of the tiles in the map. +#[derive(Resource, Clone, Copy)] +struct TilesSize(Vec2); + +impl Tile { + /// Returns the handle of the image of the tile. + fn get_texture(&self, asset_server: &AssetServer) -> Handle { + match self { + Self::Grass => asset_server.load("tiles/grass.png"), + Self::Forest => asset_server.load("tiles/forest.png"), + Self::Hill => asset_server.load("tiles/hill.png"), + } + } + + /// Returns the size of the image of the tile. + /// + /// TODO: we are currently using temporary images that will modify + /// this function in the future. + pub const fn get_image_size(&self) -> Vec2 { + match self { + Self::Grass => Vec2 { x: 184.0, y: 164.0 }, + Self::Forest => Vec2 { x: 184.0, y: 138.0 }, + Self::Hill => Vec2 { x: 184.0, y: 181.0 }, + } + } +} + +/// Init resources related to the rendering of the map. +fn init_resources_for_rendering(mut commands: Commands) { + commands.insert_resource(TilesGap(Vec2 { x: 70., y: 35. })); + commands.insert_resource(TilesSize(Vec2 { x: 125., y: 100. })) +} + +/// Renders the map. +fn render_map( + query: Query<(Entity, &TilePosition, &Tile), Changed>, + mut commands: Commands, + asset_server: Res, + tiles_gap: Res, + tiles_size: Res, +) { + for (entity, position, tile) in query.iter() { + let texture = tile.get_texture(&asset_server); + + let translation_2d = tiles_gap.0 * position.to_pixel_coordinates(); + let translation = Vec3::new( + translation_2d.x, + translation_2d.y, + z_position_from_y(translation_2d.y), + ); + + let scale_2d = tiles_size.0 / tile.get_image_size(); + + // the y scale is the same as the x scale to keep the aspect ratio. + let scale = Vec3::new(scale_2d.x, scale_2d.x, 1.0); + + commands.entity(entity).insert(SpriteBundle { + sprite: Sprite { + anchor: Anchor::BottomLeft, + ..default() + }, + texture, + transform: Transform { + translation, + scale, + ..Default::default() + }, + ..default() + }); + } +} + +/// A simple sigmoid function to convert y position to z position. +/// The return value is between 0 and 1. +fn z_position_from_y(y: f32) -> f32 { + -1.0 / (1.0 + (-y * 110_f64.powi(-3) as f32).exp()) +} diff --git a/crates/border-wars/src/responsive_scale.rs b/crates/border-wars/src/responsive_scale.rs new file mode 100644 index 0000000..dbca623 --- /dev/null +++ b/crates/border-wars/src/responsive_scale.rs @@ -0,0 +1,37 @@ +//! The file that contains the responsive scaling logic. + +use bevy::prelude::*; + +/// The plugin for the responsive scaling. +pub struct ResponsiveScalingPlugin; + +impl Plugin for ResponsiveScalingPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, init_window_size); + app.add_systems(Update, change_scaling); + } +} + +/// The default ui layout size. +#[derive(Resource)] +pub struct UILayoutSize(pub Vec2); + +/// Initializes [UILayoutSize]. +pub fn init_window_size(mut command: Commands) { + command.insert_resource(UILayoutSize(Vec2::new(1280., 720.))); +} + +/// Calculates the ui_scale.0 depending on the [UILayoutSize] +/// in order to make the ui layout responsive. +pub fn change_scaling( + mut ui_scale: ResMut, + windows: Query<&Window>, + size: Res, +) { + let window = windows.get_single().expect("Main window not found"); + let (a, b) = ( + window.resolution.width() / size.0.x, + window.resolution.height() / size.0.y, + ); + ui_scale.0 = if a < b { a } else { b } as f64 +} diff --git a/crates/border-wars/src/scenes/menu.rs b/crates/border-wars/src/scenes/menu.rs index 57e2a3a..ce81784 100644 --- a/crates/border-wars/src/scenes/menu.rs +++ b/crates/border-wars/src/scenes/menu.rs @@ -248,7 +248,7 @@ fn main_node(main_node: &mut ChildBuilder<'_, '_, '_>, asset_server: &Res>) { for entity in query.iter() { commands.entity(entity).despawn_recursive(); diff --git a/crates/border-wars/src/scenes/mod.rs b/crates/border-wars/src/scenes/mod.rs index dce2e1e..da18601 100644 --- a/crates/border-wars/src/scenes/mod.rs +++ b/crates/border-wars/src/scenes/mod.rs @@ -3,7 +3,7 @@ use bevy::prelude::*; use bevy_egui::EguiPlugin; -use crate::{change_scaling, CurrentScene}; +use crate::{responsive_scale, CurrentScene}; pub mod lobby; pub mod menu; @@ -17,6 +17,6 @@ impl Plugin for ScenesPlugin { .add_state::() .add_plugins(menu::MenuPlugin) .add_plugins(lobby::LobbyPlugin) - .add_systems(Update, change_scaling); + .add_plugins(responsive_scale::ResponsiveScalingPlugin); } }