From c71366c2e0a9f0b454957feeb87b51f6b27d54bd Mon Sep 17 00:00:00 2001
From: Alexander Foremny <aforemny@posteo.de>
Date: Fri, 15 Mar 2024 16:07:58 +0100
Subject: feat: add champions

Select champions using the `--champion` flag. Valid values are `ranged`
and `meele` (default).
---
 src/client.rs            | 45 ++++++++++++++++++++---
 src/main.rs              |  5 +++
 src/protocol.rs          |  8 ++--
 src/server.rs            | 95 ++++++++++++++++++++++++++++++------------------
 src/shared.rs            |  4 ++
 src/shared/champion.rs   | 39 ++++++++++++++++++++
 src/shared/projectile.rs |  2 -
 src/shared/stats.rs      | 16 ++++++++
 8 files changed, 169 insertions(+), 45 deletions(-)
 create mode 100644 src/shared/champion.rs
 create mode 100644 src/shared/stats.rs

diff --git a/src/client.rs b/src/client.rs
index 174d615..fff18af 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1,5 +1,6 @@
 use crate::client::network::*;
 use crate::protocol::*;
+use crate::shared::champion::*;
 use crate::shared::health::*;
 use crate::shared::imperative::*;
 use crate::shared::projectile::*;
@@ -17,13 +18,19 @@ const PLAYER_RADIUS: f32 = 10.;
 const PLAYER_HOVER_INDICATOR_RADIUS: f32 = 13.;
 const PLAYER_HOVER_RADIUS: f32 = 20.;
 
-pub fn main(server_addr: Option<SocketAddr>, client_id: u64, transport: TransportConfig) {
+pub fn main(
+    server_addr: Option<SocketAddr>,
+    client_id: u64,
+    transport: TransportConfig,
+    champion: Champion,
+) {
     App::new()
         .add_plugins(DefaultPlugins)
         .add_plugins(ClientPlugin {
             server_addr,
             client_id,
             transport,
+            champion,
         })
         .run();
 }
@@ -31,15 +38,20 @@ pub fn main(server_addr: Option<SocketAddr>, client_id: u64, transport: Transpor
 #[derive(Resource)]
 struct ClientId(pub u64);
 
+#[derive(Resource)]
+struct MyChampion(pub Champion);
+
 struct ClientPlugin {
     pub server_addr: Option<SocketAddr>,
     pub client_id: u64,
     pub transport: TransportConfig,
+    pub champion: Champion,
 }
 
 impl Plugin for ClientPlugin {
     fn build(&self, app: &mut App) {
         app.insert_resource(ClientId(self.client_id))
+            .insert_resource(MyChampion(self.champion))
             .add_plugins(NetworkPlugin {
                 server_addr: self.server_addr.clone(),
                 client_id: self.client_id,
@@ -63,9 +75,12 @@ impl Plugin for ClientPlugin {
     }
 }
 
-fn setup(mut client: ClientMut, mut commands: Commands) {
+fn setup(mut client: ClientMut, mut commands: Commands, champion: Res<MyChampion>) {
     commands.spawn(Camera2dBundle::default());
     client.connect().unwrap();
+    client
+        .send_message::<Channel1, SelectChampion>(SelectChampion(champion.0))
+        .unwrap();
 }
 
 fn render_players(
@@ -200,14 +215,22 @@ fn gizmos_attack_indicator(
     client_id: Res<ClientId>,
     hoverables: Query<(&PlayerId, &PlayerPosition)>,
     mut gizmos: Gizmos,
-    players: Query<(&PlayerId, &PlayerPosition)>,
+    player_positions: Query<(&PlayerId, &PlayerPosition)>,
+    player_champions: Query<(&PlayerId, &Champion)>,
     windows: Query<&Window>,
 ) {
-    let Some(position) = player_position(&client_id, &players) else {
+    let Some(position) = player_position(&client_id, &player_positions) else {
+        return;
+    };
+    let Some(champion) = player_champion(&client_id, &player_champions) else {
         return;
     };
     if hovered_other_player(&cameras, &client_id, &hoverables, &windows).is_some() {
-        gizmos.circle_2d(position.0, ATTACK_RANGE, Color::YELLOW);
+        gizmos.circle_2d(
+            position.0,
+            Stats::from_champion(champion).attack_range,
+            Color::YELLOW,
+        );
     }
 }
 
@@ -223,6 +246,18 @@ fn player_position(
     None
 }
 
+fn player_champion(
+    client_id: &Res<ClientId>,
+    players: &Query<(&PlayerId, &Champion)>,
+) -> Option<Champion> {
+    for (id, champion) in players.iter() {
+        if id.0 == client_id.0 {
+            return Some(*champion);
+        }
+    }
+    None
+}
+
 const HEALTH_OFFSET: f32 = 4.;
 
 fn render_health(players: Query<(&Health, &PlayerPosition)>, mut gizmos: Gizmos) {
diff --git a/src/main.rs b/src/main.rs
index b615ddc..2326310 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
+use crate::shared::champion::*;
 use clap::Parser;
 use lightyear::transport::io::TransportConfig;
 use rand::Rng;
@@ -17,6 +18,8 @@ struct Cli {
     connect_to: Option<SocketAddr>,
     #[arg(long, num_args = 0)]
     server: bool,
+    #[arg(long, default_value = "meele")]
+    champion: Champion,
 }
 
 fn main() {
@@ -38,6 +41,7 @@ fn main() {
             Some(server_addr),
             client_id as u64,
             TransportConfig::UdpSocket(client_addr),
+            cli.champion,
         );
     } else {
         let (from_server_send, from_server_recv) = crossbeam_channel::unbounded();
@@ -57,6 +61,7 @@ fn main() {
                 recv: from_server_recv,
                 send: to_server_send,
             },
+            cli.champion,
         );
     }
 }
diff --git a/src/protocol.rs b/src/protocol.rs
index 3d4567d..7ec3dcb 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -1,3 +1,4 @@
+use crate::shared::champion::*;
 use crate::shared::cooldown::*;
 use crate::shared::health::*;
 use crate::shared::imperative::*;
@@ -16,11 +17,11 @@ pub enum Inputs {
 impl UserAction for Inputs {}
 
 #[derive(Message, Serialize, Deserialize, Clone, Debug, PartialEq)]
-pub struct Message1(pub usize);
+pub struct SelectChampion(pub Champion);
 
 #[message_protocol(protocol = "MyProtocol")]
 pub enum Messages {
-    Message1(Message1),
+    SelectChampion(SelectChampion),
 }
 
 #[component_protocol(protocol = "MyProtocol")]
@@ -33,10 +34,11 @@ pub enum Components {
     ProjectilePosition(ProjectilePosition),
     Cooldown(Cooldown),
     Health(Health),
+    Champion(Champion),
 }
 
 #[derive(Channel)]
-struct Channel1;
+pub struct Channel1;
 
 protocolize! {
     Self = MyProtocol,
diff --git a/src/server.rs b/src/server.rs
index fab529f..1b9b7ef 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -1,5 +1,6 @@
 use crate::protocol::*;
 use crate::server::network::*;
+use crate::shared::champion::*;
 use crate::shared::cooldown::*;
 use crate::shared::health::*;
 use crate::shared::imperative::*;
@@ -9,6 +10,7 @@ use bevy::prelude::*;
 use bevy::utils::Duration;
 use bevy::utils::HashMap;
 use lightyear::prelude::*;
+use lightyear::server::events::MessageEvent;
 use rand::Rng;
 
 mod network;
@@ -47,6 +49,7 @@ impl Plugin for ServerPlugin {
             })
             .add_systems(Startup, setup)
             .add_systems(Update, (connects, disconnects))
+            .add_systems(Update, receive_message)
             .add_systems(Update, timers_ticket)
             .add_systems(Update, health_regen.after(timers_ticket))
             .add_systems(
@@ -94,6 +97,21 @@ fn connects(
     }
 }
 
+fn receive_message(
+    entity_map: Res<EntityMap>,
+    mut commands: Commands,
+    mut reader: EventReader<MessageEvent<SelectChampion>>,
+) {
+    for event in reader.read() {
+        let client_id = event.context();
+        let SelectChampion(champion) = event.message();
+        let Some(entity) = entity_map.0.get(client_id) else {
+            return;
+        };
+        commands.entity(*entity).insert(*champion);
+    }
+}
+
 fn disconnects(
     mut commands: Commands,
     mut disconnects: EventReader<server::DisconnectEvent>,
@@ -175,47 +193,54 @@ fn imperative_attack(
     mut cooldowns: Query<&mut Cooldown>,
     mut players: Query<(&PlayerId, &mut Imperative)>,
     mut positions: Query<&mut PlayerPosition>,
+    champions: Query<&Champion>,
     time: Res<Time>,
 ) {
     for (id, mut imperative) in players.iter_mut() {
         match *imperative {
             Imperative::Attack(target_player) => {
-                if let Some(entity) = entity_map.0.get(&id.0) {
-                    if let Some(target_entity) = entity_map.0.get(&target_player.0) {
-                        if let Ok([mut position, target_position]) =
-                            positions.get_many_mut([*entity, *target_entity])
-                        {
-                            let distance = target_position.0.distance(position.0);
-                            if distance > ATTACK_RANGE {
-                                let (new_position, _) = move_to_target(
-                                    position.0,
-                                    target_position.0,
-                                    MOVEMENT_SPEED * time.delta().as_secs_f32(),
-                                );
-                                position.0 = new_position;
-                            } else {
-                                if let Ok(mut cooldown) = cooldowns.get_mut(*entity) {
-                                    if cooldown.a_cooldown.is_zero() {
-                                        cooldown.a_cooldown = Duration::from_secs_f32(1.5);
-                                        commands.spawn(ProjectileBundle {
-                                            projectile: Projectile {
-                                                target_player,
-                                                source_player: *id,
-                                                damage: 4.,
-                                            },
-                                            position: ProjectilePosition(position.0),
-                                            replicate: Replicate::default(),
-                                        });
-                                    }
-                                } else {
-                                }
-                                *imperative = Imperative::Idle;
-                            }
-                        } else {
-                            *imperative = Imperative::Idle;
-                        }
-                    } else {
+                let Some(entity) = entity_map.0.get(&id.0) else {
+                    *imperative = Imperative::Idle;
+                    return;
+                };
+                let Ok(champion) = champions.get(*entity) else {
+                    *imperative = Imperative::Idle;
+                    return;
+                };
+                let Some(target_entity) = entity_map.0.get(&target_player.0) else {
+                    *imperative = Imperative::Idle;
+                    return;
+                };
+                let Ok([mut position, target_position]) =
+                    positions.get_many_mut([*entity, *target_entity])
+                else {
+                    *imperative = Imperative::Idle;
+                    return;
+                };
+                let distance = target_position.0.distance(position.0);
+                if distance > Stats::from_champion(*champion).attack_range {
+                    let (new_position, _) = move_to_target(
+                        position.0,
+                        target_position.0,
+                        MOVEMENT_SPEED * time.delta().as_secs_f32(),
+                    );
+                    position.0 = new_position;
+                } else {
+                    let Ok(mut cooldown) = cooldowns.get_mut(*entity) else {
                         *imperative = Imperative::Idle;
+                        return;
+                    };
+                    if cooldown.a_cooldown.is_zero() {
+                        cooldown.a_cooldown = Duration::from_secs_f32(1.5);
+                        commands.spawn(ProjectileBundle {
+                            projectile: Projectile {
+                                target_player,
+                                source_player: *id,
+                                damage: 4.,
+                            },
+                            position: ProjectilePosition(position.0),
+                            replicate: Replicate::default(),
+                        });
                     }
                 }
             }
diff --git a/src/shared.rs b/src/shared.rs
index 92b6a5b..16cea60 100644
--- a/src/shared.rs
+++ b/src/shared.rs
@@ -1,4 +1,5 @@
 use crate::protocol::Replicate;
+use crate::shared::champion::*;
 use crate::shared::cooldown::*;
 use crate::shared::health::*;
 use crate::shared::imperative::*;
@@ -8,6 +9,7 @@ use serde::Deserialize;
 use serde::Serialize;
 use std::default::Default;
 
+pub mod champion;
 pub mod cooldown;
 pub mod health;
 pub mod imperative;
@@ -28,6 +30,7 @@ pub struct PlayerBundle {
     imperative: Imperative,
     cooldown: Cooldown,
     health: Health,
+    champion: Champion,
 }
 
 #[derive(Component, Message, Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
@@ -49,6 +52,7 @@ impl PlayerBundle {
             imperative: Imperative::Idle,
             cooldown: Cooldown::default(),
             health: Health::default(),
+            champion: Champion::default(),
         }
     }
 }
diff --git a/src/shared/champion.rs b/src/shared/champion.rs
new file mode 100644
index 0000000..ec27c62
--- /dev/null
+++ b/src/shared/champion.rs
@@ -0,0 +1,39 @@
+use crate::shared::*;
+use std::str::FromStr;
+
+#[derive(Component, Message, Clone, Copy, Serialize, Deserialize, PartialEq, Debug)]
+pub enum Champion {
+    Meele,
+    Ranged,
+}
+
+impl Default for Champion {
+    fn default() -> Champion {
+        Champion::Meele
+    }
+}
+
+impl FromStr for Champion {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Champion, String> {
+        match s {
+            "ranged" => Ok(Champion::Ranged),
+            "meele" => Ok(Champion::Meele),
+            _ => Err(format!("unknown champion: {}", s)),
+        }
+    }
+}
+
+pub struct Stats {
+    pub attack_range: f32,
+}
+
+impl Stats {
+    pub fn from_champion(champion: Champion) -> Self {
+        match champion {
+            Champion::Meele => Stats { attack_range: 25. },
+            Champion::Ranged => Stats { attack_range: 60. },
+        }
+    }
+}
diff --git a/src/shared/projectile.rs b/src/shared/projectile.rs
index c42173a..2e9c6c3 100644
--- a/src/shared/projectile.rs
+++ b/src/shared/projectile.rs
@@ -1,7 +1,5 @@
 use crate::shared::*;
 
-pub const ATTACK_RANGE: f32 = 60.;
-
 #[derive(Bundle)]
 pub struct ProjectileBundle {
     pub projectile: Projectile,
diff --git a/src/shared/stats.rs b/src/shared/stats.rs
new file mode 100644
index 0000000..278a19f
--- /dev/null
+++ b/src/shared/stats.rs
@@ -0,0 +1,16 @@
+use crate::shared::champion::*;
+use crate::shared::*;
+
+#[derive(Component, Message, Clone, Serialize, Deserialize, PartialEq)]
+pub struct Stats {
+    pub attack_range: f32,
+}
+
+impl Stats {
+    pub fn from_champion(champion: Champion) -> Self {
+        match champion {
+            Champion::Meele => Stats { attack_range: 35. },
+            Champion::Ranged => Stats { attack_range: 60. },
+        }
+    }
+}
-- 
cgit v1.2.3