Co-authored-by: CoCoSol <CoCoSol007@users.noreply.github.com>
|
@ -1,2 +0,0 @@
|
||||||
[build]
|
|
||||||
rustflags = ["-Z", "threads=8"]
|
|
3995
Cargo.lock
generated
|
@ -1,18 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "border-wars"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
description = "An online turn based game."
|
|
||||||
repository = "https://git.tipragot.fr/corentin/border-wars.git"
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bevy = "0.12.1"
|
|
||||||
bevy_egui = "0.24.0"
|
|
||||||
noise = "0.8.2"
|
|
||||||
paste = "1.0.14"
|
|
||||||
serde = "1.0.197"
|
|
||||||
rand = "0.8.5"
|
|
|
@ -1,28 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
Hexagon Kit (2.0)
|
|
||||||
|
|
||||||
Created/distributed by Kenney (www.kenney.nl)
|
|
||||||
Creation date: 23-01-2024 11:58
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
License: (Creative Commons Zero, CC0)
|
|
||||||
http://creativecommons.org/publicdomain/zero/1.0/
|
|
||||||
|
|
||||||
You can use this content for personal, educational, and commercial purposes.
|
|
||||||
|
|
||||||
Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
• Website : www.kenney.nl
|
|
||||||
• Donate : www.kenney.nl/donate
|
|
||||||
|
|
||||||
• Patreon : patreon.com/kenney
|
|
||||||
|
|
||||||
Follow on social media for updates:
|
|
||||||
|
|
||||||
• Twitter: twitter.com/KenneyNL
|
|
||||||
• Instagram: instagram.com/kenney_nl
|
|
||||||
• Mastodon: mastodon.gamedev.place/@kenney
|
|
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 26 KiB |
|
@ -1,159 +0,0 @@
|
||||||
//! 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,
|
|
||||||
(keyboard_movement_system, mouse_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(400.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 keyboard_movement_system(
|
|
||||||
mut query: Query<&mut Transform, With<Camera>>,
|
|
||||||
keys: Res<Input<KeyCode>>,
|
|
||||||
keys_settings: Res<KeysMovementSettings>,
|
|
||||||
movement_speed: Res<CameraSpeedMouvement>,
|
|
||||||
delta_time: Res<Time>,
|
|
||||||
) {
|
|
||||||
for mut transform in query.iter_mut() {
|
|
||||||
let mut dx = 0.0;
|
|
||||||
let mut dy = 0.0;
|
|
||||||
for key in keys.get_pressed() {
|
|
||||||
match *key {
|
|
||||||
up if up == keys_settings.up => dy += movement_speed.0,
|
|
||||||
down if down == keys_settings.down => dy -= movement_speed.0,
|
|
||||||
right if right == keys_settings.right => dx += movement_speed.0,
|
|
||||||
left if left == keys_settings.left => dx -= movement_speed.0,
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transform.translation.x += dx * delta_time.delta_seconds();
|
|
||||||
transform.translation.y += dy * delta_time.delta_seconds();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the camera with mouse input.
|
|
||||||
fn mouse_movement_system(
|
|
||||||
mouse_button_input: Res<Input<MouseButton>>,
|
|
||||||
mut query: Query<(&mut Transform, &OrthographicProjection), With<Camera>>,
|
|
||||||
windows: Query<&Window>,
|
|
||||||
mut last_position: Local<Option<Vec2>>,
|
|
||||||
) {
|
|
||||||
let window = windows.get_single().expect("Main window not found");
|
|
||||||
let Some(position) = window.cursor_position() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if mouse_button_input.just_pressed(MouseButton::Right) {
|
|
||||||
*last_position = Some(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
if mouse_button_input.just_released(MouseButton::Right) {
|
|
||||||
*last_position = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(old_position) = *last_position {
|
|
||||||
for (mut transform, projection) in query.iter_mut() {
|
|
||||||
let offset = (old_position - position).extend(0.0) * Vec3::new(1., -1., 1.);
|
|
||||||
transform.translation += offset * projection.scale;
|
|
||||||
}
|
|
||||||
*last_position = Some(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scales the view with mouse input.
|
|
||||||
fn scale_system(
|
|
||||||
mut scroll_event: EventReader<MouseWheel>,
|
|
||||||
mut query: Query<&mut OrthographicProjection, With<Camera>>,
|
|
||||||
min_scale: Res<MinimumScale>,
|
|
||||||
max_scale: Res<MaximumScale>,
|
|
||||||
scale_speed: Res<CameraSpeedScale>,
|
|
||||||
) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
//! The file that contains utility functions, enums, structs for the game.
|
|
||||||
|
|
||||||
use bevnet::Uuid;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use networking::PlayerRank;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub mod camera;
|
|
||||||
pub mod map;
|
|
||||||
pub mod networking;
|
|
||||||
pub mod resources;
|
|
||||||
pub mod scenes;
|
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
/// A scene of the game.
|
|
||||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
|
||||||
pub enum Scene {
|
|
||||||
/// When we are in the main menu.
|
|
||||||
#[default]
|
|
||||||
Menu,
|
|
||||||
|
|
||||||
/// When we are in the lobby waiting for players to join the game.
|
|
||||||
Lobby,
|
|
||||||
|
|
||||||
/// When we play this wonderful game.
|
|
||||||
Game,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current scene of the game.
|
|
||||||
pub type CurrentScene = Scene;
|
|
||||||
|
|
||||||
/// A player in the game.
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Component, Resource, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Player {
|
|
||||||
/// The name of the player.
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
/// The rank of the player.
|
|
||||||
pub rank: PlayerRank,
|
|
||||||
|
|
||||||
/// The uuid of the player.
|
|
||||||
pub uuid: Uuid,
|
|
||||||
|
|
||||||
/// The color of the player.
|
|
||||||
pub color: (u8, u8, u8),
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
//! The main entry point of the game.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use border_wars::camera::CameraPlugin;
|
|
||||||
use border_wars::map::generation::MapGenerationPlugin;
|
|
||||||
use border_wars::map::ownership::OwnershipPlugin;
|
|
||||||
use border_wars::map::renderer::RendererPlugin;
|
|
||||||
use border_wars::map::selected_tile::SelectTilePlugin;
|
|
||||||
use border_wars::networking::NetworkingPlugin;
|
|
||||||
use border_wars::resources::ResourcesPlugin;
|
|
||||||
use border_wars::scenes::ScenesPlugin;
|
|
||||||
use border_wars::ui::UiPlugin;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
App::new()
|
|
||||||
.add_plugins(DefaultPlugins)
|
|
||||||
.add_plugins(ScenesPlugin)
|
|
||||||
.add_plugins(RendererPlugin)
|
|
||||||
.add_plugins(CameraPlugin)
|
|
||||||
.add_plugins(SelectTilePlugin)
|
|
||||||
.add_plugins(NetworkingPlugin)
|
|
||||||
.add_plugins(MapGenerationPlugin)
|
|
||||||
.add_plugins(UiPlugin)
|
|
||||||
.add_plugins(OwnershipPlugin)
|
|
||||||
.add_plugins(ResourcesPlugin)
|
|
||||||
.run();
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
//! All functions related to the generation of the map.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use noise::{NoiseFn, Perlin};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
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::<StartMapGeneration>()
|
|
||||||
.add_event::<EndMapGeneration>()
|
|
||||||
.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, Serialize, Deserialize, Clone, Copy)]
|
|
||||||
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<StartMapGeneration>,
|
|
||||||
mut end_generation_writer: EventWriter<EndMapGeneration>,
|
|
||||||
mut commands: Commands,
|
|
||||||
mut local_noise: Local<Option<Perlin>>,
|
|
||||||
mut local_spiral: Local<Option<HexSpiral<i32>>>,
|
|
||||||
) {
|
|
||||||
// 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<i32>, 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<Entity, With<Tile>>,
|
|
||||||
mut start_generation_events: EventReader<StartMapGeneration>,
|
|
||||||
) {
|
|
||||||
for _ in start_generation_events.read() {
|
|
||||||
for entity in query.iter() {
|
|
||||||
commands.entity(entity).despawn_recursive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,383 +0,0 @@
|
||||||
//! 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<Output = Self>
|
|
||||||
+ Sub<Output = Self>
|
|
||||||
+ Mul<Output = Self>
|
|
||||||
+ Div<Output = Self>
|
|
||||||
+ Rem<Output = Self>
|
|
||||||
+ Neg<Output = Self>
|
|
||||||
+ 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<T: Number>(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<T: Number>(self) -> HexPosition<T> {
|
|
||||||
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<T: Number> {
|
|
||||||
/// The current position in the ring.
|
|
||||||
current: HexPosition<T>,
|
|
||||||
|
|
||||||
/// 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<T: Number> Iterator for HexRing<T> {
|
|
||||||
type Item = HexPosition<T>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
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<usize>) {
|
|
||||||
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<T: Number> {
|
|
||||||
/// The origin of the spiral.
|
|
||||||
origin: HexPosition<T>,
|
|
||||||
|
|
||||||
/// The current ring of the spiral.
|
|
||||||
current: HexRing<T>,
|
|
||||||
|
|
||||||
/// The radius of the spiral.
|
|
||||||
radius: usize,
|
|
||||||
|
|
||||||
/// The index of the current ring in the spiral.
|
|
||||||
index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Number> Iterator for HexSpiral<T> {
|
|
||||||
type Item = HexPosition<T>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
// 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<T: Number> HexPosition<T> {
|
|
||||||
/// 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<T> {
|
|
||||||
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<T> {
|
|
||||||
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: Number> $t for HexPosition<T> {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn $n(self, rhs: Self) -> Self {
|
|
||||||
Self(self.0.$n(rhs.0), self.1.$n(rhs.1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Number> $t<T> for HexPosition<T> {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn $n(self, rhs: T) -> Self {
|
|
||||||
Self(self.0.$n(rhs), self.1.$n(rhs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Number> [< $t Assign >] for HexPosition<T> {
|
|
||||||
fn [< $n _assign >](&mut self, rhs: Self) {
|
|
||||||
self.0.[< $n _assign >](rhs.0) ;
|
|
||||||
self.1.[< $n _assign >](rhs.1) ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Number> [< $t Assign >]<T> for HexPosition<T> {
|
|
||||||
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),
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
//! Contains all the logic related to the map.
|
|
||||||
|
|
||||||
pub mod generation;
|
|
||||||
pub mod hex;
|
|
||||||
pub mod ownership;
|
|
||||||
pub mod renderer;
|
|
||||||
pub mod selected_tile;
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
use self::hex::*;
|
|
||||||
|
|
||||||
/// The position of a tile in a hexagonal map.
|
|
||||||
pub type TilePosition = HexPosition<i32>;
|
|
||||||
|
|
||||||
/// The tile of the map.
|
|
||||||
#[derive(Component, Debug)]
|
|
||||||
pub enum Tile {
|
|
||||||
/// The breeding tile.
|
|
||||||
Breeding,
|
|
||||||
|
|
||||||
/// The Casern tile.
|
|
||||||
Casern,
|
|
||||||
|
|
||||||
/// The castle tile.
|
|
||||||
Castle,
|
|
||||||
|
|
||||||
/// The hill tile.
|
|
||||||
Hill,
|
|
||||||
|
|
||||||
/// The grass tile.
|
|
||||||
Grass,
|
|
||||||
|
|
||||||
/// The forest tile.
|
|
||||||
Forest,
|
|
||||||
|
|
||||||
/// The mine tile.
|
|
||||||
Mine,
|
|
||||||
|
|
||||||
/// The outpost tile
|
|
||||||
Outpost,
|
|
||||||
|
|
||||||
/// The sawmill tile
|
|
||||||
Sawmill,
|
|
||||||
|
|
||||||
/// The tower tile
|
|
||||||
Tower,
|
|
||||||
|
|
||||||
/// The wall tile
|
|
||||||
Wall,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tile {
|
|
||||||
/// Returns the text representation of the tile.
|
|
||||||
pub fn to_text(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Breeding => "breeding".to_string(),
|
|
||||||
Self::Casern => "casern".to_string(),
|
|
||||||
Self::Castle => "castle".to_string(),
|
|
||||||
Self::Forest => "forest".to_string(),
|
|
||||||
Self::Grass => "grass".to_string(),
|
|
||||||
Self::Hill => "hill".to_string(),
|
|
||||||
Self::Mine => "mine".to_string(),
|
|
||||||
Self::Outpost => "outpost".to_string(),
|
|
||||||
Self::Sawmill => "sawmill".to_string(),
|
|
||||||
Self::Tower => "tower".to_string(),
|
|
||||||
Self::Wall => "wall".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
//! All code related to the ownership of the tiles.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
use crate::Player;
|
|
||||||
|
|
||||||
/// The owner of a tile.
|
|
||||||
#[derive(Component, Clone)]
|
|
||||||
pub struct Owner(pub Player);
|
|
||||||
|
|
||||||
/// The plugin to render the ownership of the tiles.
|
|
||||||
pub struct OwnershipPlugin;
|
|
||||||
|
|
||||||
impl Plugin for OwnershipPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Update, render_ownership);
|
|
||||||
app.add_systems(Startup, setup_ownership_resources);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The contrast of the ownership colors.
|
|
||||||
///
|
|
||||||
/// The value is a number between 0 and 1.
|
|
||||||
#[derive(Resource)]
|
|
||||||
pub struct OwnershipColorContrast(pub f32);
|
|
||||||
|
|
||||||
/// Init resources related to the ownership of the tiles.
|
|
||||||
fn setup_ownership_resources(mut commands: Commands) {
|
|
||||||
commands.insert_resource(OwnershipColorContrast(0.4));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The type condition for update ownership.
|
|
||||||
type OwnershipUpdate = Or<(Changed<Owner>, Changed<Sprite>)>;
|
|
||||||
|
|
||||||
/// Render the ownership of the tiles by applying colors.
|
|
||||||
fn render_ownership(
|
|
||||||
mut query: Query<(&mut Sprite, &Owner), OwnershipUpdate>,
|
|
||||||
contrast: Res<OwnershipColorContrast>,
|
|
||||||
) {
|
|
||||||
for (mut sprite, owner) in query.iter_mut() {
|
|
||||||
let (r, g, b) = owner.0.color;
|
|
||||||
let target = mix_colors(Color::rgb_u8(r, g, b), sprite.color, 1. - contrast.0);
|
|
||||||
|
|
||||||
sprite.color = target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mixes two colors.
|
|
||||||
fn mix_colors(color1: Color, color2: Color, alpha: f32) -> Color {
|
|
||||||
let [r1, g1, b1, _] = color1.as_rgba_u8();
|
|
||||||
let [r2, g2, b2, _] = color2.as_rgba_u8();
|
|
||||||
let mixed_r = (1.0 - alpha).mul_add(r1 as f32, alpha * r2 as f32).round() as u8;
|
|
||||||
let mixed_g = (1.0 - alpha).mul_add(g1 as f32, alpha * g2 as f32).round() as u8;
|
|
||||||
let mixed_b = (1.0 - alpha).mul_add(b1 as f32, alpha * b2 as f32).round() as u8;
|
|
||||||
Color::rgb_u8(mixed_r, mixed_g, mixed_b)
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
//! 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)]
|
|
||||||
pub struct TilesGap(pub 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<Image> {
|
|
||||||
asset_server.load(format!("tiles/{}.png", self.to_text()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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::Breeding => Vec2::new(184., 158.),
|
|
||||||
Self::Casern => Vec2::new(184., 167.),
|
|
||||||
Self::Castle => Vec2::new(192., 196.),
|
|
||||||
Self::Forest => Vec2::new(184., 165.),
|
|
||||||
Self::Grass => Vec2::new(184., 138.),
|
|
||||||
Self::Hill => Vec2::new(184., 181.),
|
|
||||||
Self::Mine => Vec2::new(184., 166.),
|
|
||||||
Self::Outpost => Vec2::new(184., 208.),
|
|
||||||
Self::Sawmill => Vec2::new(184., 138.),
|
|
||||||
Self::Tower => Vec2::new(184., 218.),
|
|
||||||
Self::Wall => Vec2::new(184., 186.),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Tile>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
asset_server: Res<AssetServer>,
|
|
||||||
tiles_gap: Res<TilesGap>,
|
|
||||||
tiles_size: Res<TilesSize>,
|
|
||||||
) {
|
|
||||||
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::BottomCenter,
|
|
||||||
..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())
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
//! All programs related to the selection of a tile.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
use super::renderer::TilesGap;
|
|
||||||
use super::Tile;
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// The currently selected tile.
|
|
||||||
#[derive(Resource, Default, Debug)]
|
|
||||||
pub enum SelectedTile {
|
|
||||||
/// The entity of the selected tile.
|
|
||||||
Tile(Entity),
|
|
||||||
|
|
||||||
/// Zero tile selected.
|
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SelectedTile {
|
|
||||||
/// Returns the entity of the selected tile.
|
|
||||||
/// Returns `None` if no tile is selected.
|
|
||||||
pub const fn get_entity(&self) -> Option<Entity> {
|
|
||||||
match self {
|
|
||||||
Self::Tile(entity) => Some(*entity),
|
|
||||||
Self::None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A plugin that handles the selection of tiles.
|
|
||||||
pub struct SelectTilePlugin;
|
|
||||||
|
|
||||||
impl Plugin for SelectTilePlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(PreUpdate, mouse_handler)
|
|
||||||
.add_systems(PreUpdate, select_closest_tile)
|
|
||||||
.add_event::<ClickOnTheWorld>()
|
|
||||||
.init_resource::<SelectedTile>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Input<MouseButton>>,
|
|
||||||
windows: Query<&Window>,
|
|
||||||
cameras: Query<(&Camera, &GlobalTransform)>,
|
|
||||||
mut events_writer: EventWriter<ClickOnTheWorld>,
|
|
||||||
not_clickable_zones: Query<(&Node, &GlobalTransform), With<ZoneNotClickable>>,
|
|
||||||
ui_scale: Res<UiScale>,
|
|
||||||
) {
|
|
||||||
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 select it.
|
|
||||||
fn select_closest_tile(
|
|
||||||
tiles: Query<(Entity, &Transform, &Tile)>,
|
|
||||||
mut click_event_reader: EventReader<ClickOnTheWorld>,
|
|
||||||
tile_gap: Res<TilesGap>,
|
|
||||||
mut current_entity: ResMut<SelectedTile>,
|
|
||||||
) {
|
|
||||||
for click_event in click_event_reader.read() {
|
|
||||||
// The closest tile and its position.
|
|
||||||
let mut closest_entity: Option<Entity> = None;
|
|
||||||
let mut closest_position: Option<f32> = None;
|
|
||||||
|
|
||||||
// To keep the aspect ratio.
|
|
||||||
let click_position = click_event.0 / tile_gap.0;
|
|
||||||
|
|
||||||
for (tile_entity, tile_transform, tile_type) in tiles.iter() {
|
|
||||||
let tile_size = tile_type.get_image_size();
|
|
||||||
let tile_scale = tile_transform.scale.truncate();
|
|
||||||
|
|
||||||
let mut tile_position = tile_transform.translation.truncate() / tile_gap.0;
|
|
||||||
// The origine of the tile is the bottom center.
|
|
||||||
tile_position.y += (tile_size.y / 2.0) * tile_scale.y / tile_gap.0.y;
|
|
||||||
|
|
||||||
let distance_to_cursor = tile_position.distance(click_position);
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if current_entity.get_entity() == Some(tile_entity) {
|
|
||||||
*current_entity = SelectedTile::None;
|
|
||||||
} else {
|
|
||||||
*current_entity = SelectedTile::Tile(tile_entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
//! All the code related to the check connection (check every X seconds if any
|
|
||||||
//! player is still connected).
|
|
||||||
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use bevnet::{Connection, NetworkAppExt, Receive, SendTo};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy::utils::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::Player;
|
|
||||||
|
|
||||||
/// A plugin that check if a player is still connected.
|
|
||||||
pub struct CheckConnectionPlugin;
|
|
||||||
|
|
||||||
/// An event that is trigger when a player is disconnected.
|
|
||||||
#[derive(Event)]
|
|
||||||
pub struct PlayerDisconnected(pub Player);
|
|
||||||
|
|
||||||
/// An event that is send between all players to check if a player is still
|
|
||||||
/// connected.
|
|
||||||
#[derive(Event, Serialize, Deserialize)]
|
|
||||||
struct IAmConnected(Player);
|
|
||||||
|
|
||||||
impl Plugin for CheckConnectionPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(
|
|
||||||
Update,
|
|
||||||
(
|
|
||||||
check_connection,
|
|
||||||
send_check_connection,
|
|
||||||
handle_disconnect_player,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.add_event::<PlayerDisconnected>()
|
|
||||||
.add_network_event::<IAmConnected>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The interval to check if a player is still connected.
|
|
||||||
/// We put this into a const because we don't want to change it.
|
|
||||||
const CHECK_CONNECTION_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
|
|
||||||
|
|
||||||
/// A fonction that check if a player is still connected.
|
|
||||||
fn check_connection(
|
|
||||||
all_players_query: Query<&Player>,
|
|
||||||
mut disconnect_event: EventWriter<PlayerDisconnected>,
|
|
||||||
mut checked_players: Local<HashMap<Player, Instant>>,
|
|
||||||
mut connect_event: EventReader<Receive<IAmConnected>>,
|
|
||||||
) {
|
|
||||||
for Receive(_, IAmConnected(player)) in connect_event.read() {
|
|
||||||
checked_players.insert(player.clone(), Instant::now());
|
|
||||||
}
|
|
||||||
for player in all_players_query.iter() {
|
|
||||||
if !(*checked_players).contains_key(player) {
|
|
||||||
checked_players.insert(player.clone(), Instant::now());
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(last_seen) = (*checked_players).get_mut(player) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if last_seen.elapsed() > CHECK_CONNECTION_INTERVAL {
|
|
||||||
disconnect_event.send(PlayerDisconnected(player.clone()));
|
|
||||||
checked_players.remove(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simple time/instant that implement Default.
|
|
||||||
struct Time(std::time::Instant);
|
|
||||||
|
|
||||||
impl Default for Time {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(std::time::Instant::now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fonction that send a check connection event to all players.
|
|
||||||
fn send_check_connection(
|
|
||||||
mut check_connection_event: EventWriter<SendTo<IAmConnected>>,
|
|
||||||
all_players_query: Query<&Player>,
|
|
||||||
connection: Res<Connection>,
|
|
||||||
mut timer: Local<Time>,
|
|
||||||
) {
|
|
||||||
if timer.0.elapsed() < CHECK_CONNECTION_INTERVAL / 2 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(self_player) = all_players_query
|
|
||||||
.iter()
|
|
||||||
.find(|player| connection.identifier() == Some(player.uuid))
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
for player in all_players_query.iter() {
|
|
||||||
check_connection_event.send(SendTo(player.uuid, IAmConnected(self_player.clone())));
|
|
||||||
}
|
|
||||||
|
|
||||||
timer.0 = std::time::Instant::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fonction that handle player disconnection.
|
|
||||||
fn handle_disconnect_player(
|
|
||||||
mut disconnect_players: EventReader<PlayerDisconnected>,
|
|
||||||
all_players_query: Query<(&Player, Entity)>,
|
|
||||||
mut commands: Commands,
|
|
||||||
) {
|
|
||||||
for PlayerDisconnected(disconnect_player) in disconnect_players.read() {
|
|
||||||
let Some((_, entity)) = all_players_query
|
|
||||||
.iter()
|
|
||||||
.find(|(player, _entity)| *player == disconnect_player)
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
commands.entity(entity).despawn();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
//! All the code related to the connection.
|
|
||||||
|
|
||||||
use bevnet::{Connection, NetworkAppExt, Receive, SendTo};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::PlayerRank;
|
|
||||||
use crate::{CurrentScene, Player};
|
|
||||||
|
|
||||||
/// A plugin that manage connections (add, remove).
|
|
||||||
pub struct ConnectionPlugin;
|
|
||||||
|
|
||||||
impl Plugin for ConnectionPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_network_event::<RequestJoin>()
|
|
||||||
.add_network_event::<AddPlayer>()
|
|
||||||
.add_network_event::<RemovePlayer>()
|
|
||||||
.add_systems(
|
|
||||||
Update,
|
|
||||||
(accept_connection, handle_new_player, handle_remove_player),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An event that is trigger when a new player request to join a game.
|
|
||||||
#[derive(Event, Serialize, Deserialize)]
|
|
||||||
pub struct RequestJoin(pub Player);
|
|
||||||
|
|
||||||
/// An event that is trigger when a new player is added.
|
|
||||||
#[derive(Event, Serialize, Deserialize)]
|
|
||||||
pub struct AddPlayer(Player);
|
|
||||||
|
|
||||||
/// An event that is trigger when a player is removed.
|
|
||||||
#[derive(Event, Serialize, Deserialize)]
|
|
||||||
pub struct RemovePlayer(pub Player);
|
|
||||||
|
|
||||||
/// A fonction that accept new connection.
|
|
||||||
/// It add the player to the list of all players.
|
|
||||||
pub fn accept_connection(
|
|
||||||
all_players_query: Query<&Player>,
|
|
||||||
mut requests_join_event: EventReader<Receive<RequestJoin>>,
|
|
||||||
mut add_players_event: EventWriter<SendTo<AddPlayer>>,
|
|
||||||
state: Res<State<CurrentScene>>,
|
|
||||||
) {
|
|
||||||
for request_join in requests_join_event.read() {
|
|
||||||
let mut new_player = request_join.1.0.clone();
|
|
||||||
|
|
||||||
let current_state = *state.get();
|
|
||||||
|
|
||||||
if current_state == CurrentScene::Menu {
|
|
||||||
return;
|
|
||||||
} else if current_state == CurrentScene::Game {
|
|
||||||
new_player.rank = PlayerRank::Spectator;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_players_event.send(SendTo(new_player.uuid, AddPlayer(new_player.clone())));
|
|
||||||
|
|
||||||
for old_player in all_players_query.iter() {
|
|
||||||
// Link all players
|
|
||||||
add_players_event.send(SendTo(old_player.uuid, AddPlayer(new_player.clone())));
|
|
||||||
add_players_event.send(SendTo(new_player.uuid, AddPlayer(old_player.clone())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fonction that handle new players when a events is received.
|
|
||||||
pub fn handle_new_player(mut add_players: EventReader<Receive<AddPlayer>>, mut commands: Commands) {
|
|
||||||
for add_player in add_players.read() {
|
|
||||||
commands.spawn(add_player.1.0.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fonction that handle remove players when a events is received.
|
|
||||||
pub fn handle_remove_player(
|
|
||||||
mut remove_players: EventReader<Receive<RemovePlayer>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
all_players_query: Query<(Entity, &Player)>,
|
|
||||||
connection: Res<Connection>,
|
|
||||||
mut next_scene: ResMut<NextState<CurrentScene>>,
|
|
||||||
) {
|
|
||||||
for remove_player in remove_players.read() {
|
|
||||||
if Some(remove_player.1.0.uuid) == connection.identifier() {
|
|
||||||
next_scene.set(CurrentScene::Menu);
|
|
||||||
all_players_query.iter().for_each(|(entity, _)| {
|
|
||||||
commands.entity(entity).despawn();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (entity, player) in all_players_query.iter() {
|
|
||||||
if remove_player.1.0.uuid == player.uuid {
|
|
||||||
commands.entity(entity).despawn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
//! All the code related to the networking.
|
|
||||||
|
|
||||||
use bevnet::{NetworkAppExt, NetworkPlugin, Receive};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use self::check_connection::CheckConnectionPlugin;
|
|
||||||
use self::connection::ConnectionPlugin;
|
|
||||||
use crate::map::generation::StartMapGeneration;
|
|
||||||
use crate::CurrentScene;
|
|
||||||
|
|
||||||
pub mod check_connection;
|
|
||||||
pub mod connection;
|
|
||||||
|
|
||||||
/// The plugin for the networking.
|
|
||||||
pub struct NetworkingPlugin;
|
|
||||||
|
|
||||||
impl Plugin for NetworkingPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_plugins(NetworkPlugin::new("relay.cocosol.fr".to_string()))
|
|
||||||
.add_plugins(ConnectionPlugin)
|
|
||||||
.add_systems(Update, handle_start_game)
|
|
||||||
.add_network_event::<StartGame>()
|
|
||||||
.add_plugins(CheckConnectionPlugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The rank of the player.
|
|
||||||
#[derive(PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Debug, Hash)]
|
|
||||||
pub enum PlayerRank {
|
|
||||||
/// A spectator. He does not play the game, just renderer the game.
|
|
||||||
Spectator,
|
|
||||||
|
|
||||||
/// An admin. He manages the game and play the game.
|
|
||||||
Admin,
|
|
||||||
|
|
||||||
/// The player. He can join the game and play.
|
|
||||||
Player,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The event to start the game, that is send by the admin.
|
|
||||||
#[derive(Event, Serialize, Deserialize)]
|
|
||||||
pub struct StartGame(pub StartMapGeneration);
|
|
||||||
|
|
||||||
/// A fonction that handle the start of the game.
|
|
||||||
fn handle_start_game(
|
|
||||||
mut next_stats: ResMut<NextState<CurrentScene>>,
|
|
||||||
mut start_game_events: EventReader<Receive<StartGame>>,
|
|
||||||
mut start_map_generation_writer: EventWriter<StartMapGeneration>,
|
|
||||||
) {
|
|
||||||
for event in start_game_events.read() {
|
|
||||||
next_stats.set(CurrentScene::Game);
|
|
||||||
start_map_generation_writer.send(event.1.0);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
//! All program related to the resources of the game.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
/// The plugin that manage the resources.
|
|
||||||
pub struct ResourcesPlugin;
|
|
||||||
|
|
||||||
impl Plugin for ResourcesPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_event::<ResetResources>()
|
|
||||||
.insert_resource(Resources::initial())
|
|
||||||
.add_systems(Update, handle_reset_resources);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The resources of the game.
|
|
||||||
#[derive(Resource, Default)]
|
|
||||||
pub struct Resources {
|
|
||||||
/// The stone resource.
|
|
||||||
pub stone: u32,
|
|
||||||
|
|
||||||
/// The wood resource.
|
|
||||||
pub wood: u32,
|
|
||||||
|
|
||||||
/// The food resource.
|
|
||||||
pub food: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resources {
|
|
||||||
/// Returns the initial resources of the game.
|
|
||||||
const fn initial() -> Self {
|
|
||||||
Self {
|
|
||||||
stone: 100,
|
|
||||||
wood: 100,
|
|
||||||
food: 100,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An event send to reset the resources of the game.
|
|
||||||
#[derive(Event)]
|
|
||||||
pub struct ResetResources;
|
|
||||||
|
|
||||||
/// Handles the reset resources event.
|
|
||||||
fn handle_reset_resources(
|
|
||||||
mut reset_resources_event: EventReader<ResetResources>,
|
|
||||||
mut resources: ResMut<Resources>,
|
|
||||||
) {
|
|
||||||
for _ in reset_resources_event.read() {
|
|
||||||
*resources = Resources::initial();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
//! The lobby of the game.
|
|
||||||
|
|
||||||
use bevnet::{Connection, SendTo};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy_egui::{egui, EguiContexts};
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
use crate::map::generation::StartMapGeneration;
|
|
||||||
use crate::networking::connection::RemovePlayer;
|
|
||||||
use crate::networking::{PlayerRank, StartGame};
|
|
||||||
use crate::{CurrentScene, Player};
|
|
||||||
|
|
||||||
/// The plugin for the lobby.
|
|
||||||
pub struct LobbyPlugin;
|
|
||||||
|
|
||||||
impl Plugin for LobbyPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Update, lobby_ui.run_if(in_state(CurrentScene::Lobby)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display the UI of the lobby.
|
|
||||||
fn lobby_ui(
|
|
||||||
mut ctx: EguiContexts,
|
|
||||||
connection: Res<Connection>,
|
|
||||||
all_players_query: Query<&Player>,
|
|
||||||
mut kick_player: EventWriter<SendTo<RemovePlayer>>,
|
|
||||||
mut map_size: Local<u32>,
|
|
||||||
mut start_game_event: EventWriter<SendTo<StartGame>>,
|
|
||||||
) {
|
|
||||||
// Get our player info.
|
|
||||||
let Some(self_player) = all_players_query
|
|
||||||
.iter()
|
|
||||||
.find(|player| connection.identifier() == Some(player.uuid))
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx.ctx_mut(), |ui| {
|
|
||||||
ui.heading("Border Wars");
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.label("Game created");
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if self_player.rank != PlayerRank::Admin {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ui.label("Game ID: ");
|
|
||||||
ui.text_edit_singleline(&mut connection.identifier().unwrap_or_default().to_string());
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
for player in all_players_query.iter() {
|
|
||||||
ui.label(player.name.to_string());
|
|
||||||
if self_player.rank == PlayerRank::Admin
|
|
||||||
&& player.rank != PlayerRank::Admin
|
|
||||||
&& ui.button("Remove").clicked()
|
|
||||||
{
|
|
||||||
for sender_id in all_players_query.iter() {
|
|
||||||
kick_player.send(SendTo(sender_id.uuid, RemovePlayer(player.clone())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ui.separator();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self_player.rank != PlayerRank::Admin {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add(egui::Slider::new(&mut (*map_size), 1..=3).text("map size"));
|
|
||||||
|
|
||||||
if !ui.button("Run the game").clicked() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seed = rand::thread_rng().gen::<u32>();
|
|
||||||
let index = *map_size as u16;
|
|
||||||
let nomber_of_players = all_players_query.iter().count() as u32;
|
|
||||||
|
|
||||||
let radius = nomber_of_players as u16 * 2 * (index + 1);
|
|
||||||
|
|
||||||
// Start the game.
|
|
||||||
for player in all_players_query.iter() {
|
|
||||||
start_game_event.send(SendTo(
|
|
||||||
player.uuid,
|
|
||||||
StartGame(StartMapGeneration { seed, radius }),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
//! The main menu of the game.
|
|
||||||
|
|
||||||
use bevnet::{Connection, SendTo, Uuid};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy_egui::{egui, EguiContexts};
|
|
||||||
|
|
||||||
use crate::networking::connection::RequestJoin;
|
|
||||||
use crate::networking::PlayerRank;
|
|
||||||
use crate::{CurrentScene, Player};
|
|
||||||
|
|
||||||
/// The plugin for the menu.
|
|
||||||
pub struct MenuPlugin;
|
|
||||||
|
|
||||||
impl Plugin for MenuPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Update, menu_ui.run_if(in_state(CurrentScene::Menu)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display the UI of the menu to host a game or join one.
|
|
||||||
fn menu_ui(
|
|
||||||
mut ctx: EguiContexts,
|
|
||||||
mut connection_string: Local<String>,
|
|
||||||
mut next_scene: ResMut<NextState<CurrentScene>>,
|
|
||||||
mut request_join: EventWriter<SendTo<RequestJoin>>,
|
|
||||||
mut name: Local<String>,
|
|
||||||
connection: Res<Connection>,
|
|
||||||
mut commands: Commands,
|
|
||||||
) {
|
|
||||||
let Some(uuid) = connection.identifier() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx.ctx_mut(), |ui| {
|
|
||||||
ui.heading("Border Wars");
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.label("Name");
|
|
||||||
ui.text_edit_singleline(&mut *name);
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.label("Connect to an existing game:");
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Game ID: ");
|
|
||||||
ui.text_edit_singleline(&mut *connection_string);
|
|
||||||
|
|
||||||
let Ok(game_id) = Uuid::parse_str(&connection_string) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ui.button("Join").clicked() {
|
|
||||||
next_scene.set(CurrentScene::Lobby);
|
|
||||||
request_join.send(SendTo(
|
|
||||||
game_id,
|
|
||||||
RequestJoin(Player {
|
|
||||||
name: name.clone(),
|
|
||||||
rank: PlayerRank::Player,
|
|
||||||
uuid,
|
|
||||||
color: rand::random::<(u8, u8, u8)>(),
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if ui.button("Create new game").clicked() {
|
|
||||||
next_scene.set(CurrentScene::Lobby);
|
|
||||||
commands.spawn(Player {
|
|
||||||
name: name.clone(),
|
|
||||||
rank: PlayerRank::Admin,
|
|
||||||
uuid,
|
|
||||||
color: rand::random::<(u8, u8, u8)>(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
//! The file containing all scenes programs.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy_egui::EguiPlugin;
|
|
||||||
|
|
||||||
use crate::CurrentScene;
|
|
||||||
|
|
||||||
pub mod lobby;
|
|
||||||
pub mod menu;
|
|
||||||
|
|
||||||
/// The plugin for all scenes.
|
|
||||||
pub struct ScenesPlugin;
|
|
||||||
|
|
||||||
impl Plugin for ScenesPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_plugins(EguiPlugin)
|
|
||||||
.add_state::<CurrentScene>()
|
|
||||||
.add_plugins(menu::MenuPlugin)
|
|
||||||
.add_plugins(lobby::LobbyPlugin);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
//! The file that contains the hover logic.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
/// The plugin for the hover system.
|
|
||||||
pub struct HoverPlugin;
|
|
||||||
|
|
||||||
impl Plugin for HoverPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Update, hovering);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A component that stores the hover texture and the original texture.
|
|
||||||
#[derive(Component, Clone)]
|
|
||||||
pub struct HoveredTexture {
|
|
||||||
/// The original texture.
|
|
||||||
pub texture: Handle<Image>,
|
|
||||||
|
|
||||||
/// The hovered texture.
|
|
||||||
pub hovered_texture: Handle<Image>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The system that applies the hover logic by changing the texture.
|
|
||||||
fn hovering(
|
|
||||||
mut interaction_query: Query<
|
|
||||||
(&Interaction, &HoveredTexture, &mut UiImage),
|
|
||||||
Changed<Interaction>,
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
for (interaction, textures, mut image) in interaction_query.iter_mut() {
|
|
||||||
match *interaction {
|
|
||||||
Interaction::Hovered => image.texture = textures.hovered_texture.clone(),
|
|
||||||
Interaction::None => image.texture = textures.texture.clone(),
|
|
||||||
Interaction::Pressed => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
//! The file that contains the UI logic.
|
|
||||||
|
|
||||||
pub mod hover;
|
|
||||||
pub mod responsive_scale;
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
use self::hover::HoverPlugin;
|
|
||||||
use self::responsive_scale::ResponsiveScalingPlugin;
|
|
||||||
|
|
||||||
/// The plugin for the UI.
|
|
||||||
pub struct UiPlugin;
|
|
||||||
|
|
||||||
impl Plugin for UiPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_plugins(HoverPlugin)
|
|
||||||
.add_plugins(ResponsiveScalingPlugin);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
//! 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<UiScale>,
|
|
||||||
windows: Query<&Window>,
|
|
||||||
size: Res<UILayoutSize>,
|
|
||||||
) {
|
|
||||||
let window = windows.get_single().expect("Main window not found");
|
|
||||||
if window.resolution.physical_height() == 0 {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -6,5 +6,16 @@ license = "GPL-3.0-or-later"
|
||||||
description = "The server of Border Wars"
|
description = "The server of Border Wars"
|
||||||
repository = "https://git.tipragot.fr/corentin/border-wars.git"
|
repository = "https://git.tipragot.fr/corentin/border-wars.git"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.81"
|
||||||
|
axum = { version = "0.7.5", features = ["ws"] }
|
||||||
|
bincode = "1.3.3"
|
||||||
|
dashmap = "5.5.3"
|
||||||
|
futures = "0.3.30"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
uuid = { version = "1.8.0", features = ["v4"] }
|
||||||
|
|
||||||
# [lints]
|
# [lints]
|
||||||
# workspace = true
|
# workspace = true
|
||||||
|
|
26
crates/server/src/lib.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
struct Action {
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
tile_id: Uuid,
|
||||||
|
image:
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActionType {}
|
||||||
|
|
||||||
|
struct Player {}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
async fn make_action(&mut self, actions: HashSet<Action>) -> Action {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn border_wars_classic(players: HashSet<Player>) {
|
||||||
|
let player1 = players.iter().next().unwrap();
|
||||||
|
|
||||||
|
let action = player1.make_action(HashSet::new()).await;
|
||||||
|
}
|
|
@ -1,3 +1,78 @@
|
||||||
fn main() {
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
println!("Hello, world!");
|
use axum::extract::WebSocketUpgrade;
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::Router;
|
||||||
|
use dashmap::mapref::entry::Entry;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tokio::sync::mpsc::{channel, Sender};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
trait Request: DeserializeOwned + Serialize {
|
||||||
|
type Response: Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Response: DeserializeOwned + Serialize {}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref CLIENTS: DashMap<Uuid, Sender<Vec<u8>>> = DashMap::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let app = Router::new().route(
|
||||||
|
"/",
|
||||||
|
get(|ws: WebSocketUpgrade| async { ws.on_upgrade(|socket| handle(socket)) }),
|
||||||
|
);
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:80")
|
||||||
|
.await
|
||||||
|
.expect("failed to bind");
|
||||||
|
axum::serve(listener, app).await.expect("failed to serve");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(mut socket: WebSocket) {
|
||||||
|
let (mut writer, mut reader) = socket.split();
|
||||||
|
|
||||||
|
let (sender, mut receiver) = channel(128);
|
||||||
|
let client_id = loop {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
let Entry::Vacant(entry) = CLIENTS.entry(id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
entry.insert(sender);
|
||||||
|
break id;
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(message) = receiver.recv().await {
|
||||||
|
writer.send(Message::Binary(message)).await?;
|
||||||
|
}
|
||||||
|
Ok::<(), axum::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
while let Some(Ok(message)) = reader.next().await {
|
||||||
|
let Message::Binary(data) = message else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Err(error) = message_received(client_id, data).await {
|
||||||
|
println!("Error: {}", error);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CLIENTS.remove(&client_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_message(id: Uuid, message: Vec<u8>) -> anyhow::Result<()> {
|
||||||
|
if let Some(sender) = CLIENTS.get(&id) {
|
||||||
|
sender.send(message).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn message_received(id: Uuid, message: Vec<u8>) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|