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"
|
||||
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]
|
||||
# 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() {
|
||||
println!("Hello, world!");
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
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(())
|
||||
}
|
||||
|
|