DADA
Some checks failed
Rust Checks / checks (push) Has been cancelled

Co-authored-by: CoCoSol <CoCoSol007@users.noreply.github.com>
This commit is contained in:
Tipragot 2024-04-08 19:53:21 +02:00
parent 1363883dd5
commit 63afb174cf
37 changed files with 526 additions and 5319 deletions

View file

@ -1,2 +0,0 @@
[build]
rustflags = ["-Z", "threads=8"]

3995
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View file

@ -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;
}
}
}
}

View file

@ -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),
}

View file

@ -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();
}

View file

@ -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();
}
}
}

View file

@ -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),
}

View file

@ -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(),
}
}
}

View file

@ -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)
}

View file

@ -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())
}

View file

@ -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);
}
}
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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 }),
));
}
});
}

View file

@ -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)>(),
});
}
});
}

View file

@ -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);
}
}

View file

@ -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 => (),
}
}
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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
View 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;
}

View file

@ -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(())
}