From 9d8bf0fde7b12fa77569333d17f310e55df58db9 Mon Sep 17 00:00:00 2001
From: Alexander Foremny <aforemny@posteo.de>
Date: Tue, 19 Mar 2024 06:52:42 +0100
Subject: feat: focus ability

---
 src/server.rs            | 47 +++++++++++++++++-----------
 src/server/entity_map.rs |  6 ++++
 src/shared/ability.rs    | 81 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/shared/buffs.rs      |  1 +
 src/shared/champion.rs   | 19 +++++++-----
 src/shared/cooldown.rs   |  3 ++
 src/shared/stats.rs      |  3 +-
 7 files changed, 134 insertions(+), 26 deletions(-)
 create mode 100644 src/server/entity_map.rs

diff --git a/src/server.rs b/src/server.rs
index b3c2a30..483314d 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -1,4 +1,5 @@
 use crate::protocol::*;
+use crate::server::entity_map::*;
 use crate::server::network::*;
 use crate::shared::ability::*;
 use crate::shared::activation::*;
@@ -13,16 +14,14 @@ use crate::shared::projectile::*;
 use crate::shared::stats::*;
 use crate::shared::*;
 use bevy::prelude::*;
-use bevy::utils::HashMap;
+use bevy::utils::*;
 use lightyear::prelude::*;
 use lightyear::server::events::MessageEvent;
 use rand::Rng;
 
+pub mod entity_map;
 mod network;
 
-#[derive(Resource, Default)]
-struct EntityMap(HashMap<ClientId, Entity>);
-
 pub fn main(transport: TransportConfig) {
     App::new()
         .add_plugins(MinimalPlugins)
@@ -284,7 +283,11 @@ fn imperative_attack_attack(
                     };
                     let base_cooldown = champion.base_cooldown();
                     if cooldown.0[ability_slot].is_zero() {
-                        cooldown.0[ability_slot] = base_cooldown.0[ability_slot];
+                        cooldown.0[ability_slot] = if ability_slot == AbilitySlot::A {
+                            Duration::from_secs_f32(effective_stats.0.attack_speed)
+                        } else {
+                            base_cooldown.0[ability_slot]
+                        };
                         commands.spawn(ProjectileBundle::new(ability.to_projectile(
                             *id,
                             position.0,
@@ -318,12 +321,13 @@ fn imperative_attack_attack(
 }
 
 fn activation(
+    champions: Query<&Champion>,
+    mut commands: Commands,
     entity_map: Res<EntityMap>,
     mut cooldowns: Query<&mut Cooldown>,
-    mut players: Query<(&PlayerId, &mut Activation, &mut Buffs)>,
-    champions: Query<&Champion>,
+    mut players: Query<(&PlayerId, &mut Activation)>,
 ) {
-    for (id, mut activation, mut buffs) in players.iter_mut() {
+    for (id, mut activation) in players.iter_mut() {
         let Some(entity) = entity_map.0.get(&id.0) else {
             *activation = Activation::None;
             continue;
@@ -344,15 +348,11 @@ fn activation(
             *activation = Activation::None;
             continue;
         };
-        match ability {
-            ActivatedAbility::Speed(Speed { duration }) => {
-                let base_cooldown = champion.base_cooldown();
-                if cooldown.0[ability_slot].is_zero() {
-                    cooldown.0[ability_slot] = base_cooldown.0[ability_slot];
-                    buffs.speed = Some(Timer::from_seconds(duration, TimerMode::Once));
-                    *activation = Activation::None;
-                }
-            }
+        let base_cooldown = champion.base_cooldown();
+        if cooldown.0[ability_slot].is_zero() {
+            cooldown.0[ability_slot] = base_cooldown.0[ability_slot];
+            ability.activate()(&mut commands, *id);
+            *activation = Activation::None;
         }
     }
 }
@@ -440,7 +440,7 @@ fn projectile_despawn(
                         free_projectile
                             .position
                             .distance(free_projectile.starting_position)
-                            >= 10000.0,
+                            >= free_projectile.max_distance,
                         None,
                     )
                 }
@@ -527,6 +527,9 @@ fn effective_stats(
         if buffs.speed.is_some() {
             stats.movement_speed *= 1.25;
         }
+        if buffs.haste.is_some() {
+            stats.attack_speed *= 0.5;
+        }
         effective_stats.0 = stats;
         health.0 = health.0.min(effective_stats.0.max_health);
     }
@@ -535,6 +538,9 @@ fn effective_stats(
 fn buffs_tick(mut buffses: Query<&mut Buffs>, time: Res<Time>) {
     let dt = time.delta();
     for mut buffs in buffses.iter_mut() {
+        if let Some(ref mut timer) = &mut buffs.haste {
+            timer.tick(dt);
+        }
         if let Some(ref mut timer) = &mut buffs.slow {
             timer.tick(dt);
         }
@@ -546,6 +552,11 @@ fn buffs_tick(mut buffses: Query<&mut Buffs>, time: Res<Time>) {
 
 fn buffs_despawn(mut buffses: Query<&mut Buffs>) {
     for mut buffs in buffses.iter_mut() {
+        if let Some(timer) = &buffs.haste {
+            if timer.finished() {
+                buffs.haste = None;
+            }
+        }
         if let Some(timer) = &buffs.slow {
             if timer.finished() {
                 buffs.slow = None;
diff --git a/src/server/entity_map.rs b/src/server/entity_map.rs
new file mode 100644
index 0000000..13225fb
--- /dev/null
+++ b/src/server/entity_map.rs
@@ -0,0 +1,6 @@
+use bevy::prelude::*;
+use bevy::utils::*;
+use lightyear::prelude::*;
+
+#[derive(Resource, Default)]
+pub struct EntityMap(pub HashMap<ClientId, Entity>);
diff --git a/src/shared/ability.rs b/src/shared/ability.rs
index 4d45a4d..b841661 100644
--- a/src/shared/ability.rs
+++ b/src/shared/ability.rs
@@ -1,3 +1,5 @@
+use crate::server::entity_map::*;
+use crate::shared::buffs::*;
 use crate::shared::player::*;
 use crate::shared::projectile::*;
 use crate::shared::*;
@@ -55,14 +57,93 @@ impl TargetedAbility {
 
 #[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
 pub enum ActivatedAbility {
+    Focus(Focus),
+    Haste(Haste),
     Speed(Speed),
 }
 
+#[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
+pub struct Focus {
+    pub duration: f32,
+}
+
+#[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
+pub struct Haste {
+    pub duration: f32,
+}
+
 #[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
 pub struct Speed {
     pub duration: f32,
 }
 
+pub type ActivatedAbilityActivation = Box<dyn FnOnce(&mut Commands, PlayerId) -> ()>;
+
+impl ActivatedAbility {
+    pub fn activate(self) -> ActivatedAbilityActivation {
+        match self {
+            ActivatedAbility::Focus(focus) => focus_activation(focus),
+            ActivatedAbility::Haste(haste) => haste_activation(haste),
+            ActivatedAbility::Speed(speed) => speed_activation(speed),
+        }
+    }
+}
+
+fn speed_activation(speed: Speed) -> ActivatedAbilityActivation {
+    Box::new(move |commands: &mut Commands, source_id: PlayerId| {
+        commands.add(move |world: &mut World| {
+            world.run_system_once(
+                move |entity_map: Res<EntityMap>, mut buffses: Query<&mut Buffs>| {
+                    let Some(entity_id) = entity_map.0.get(&source_id.0) else {
+                        return;
+                    };
+                    let Ok(mut buffs) = buffses.get_mut(*entity_id) else {
+                        return;
+                    };
+                    buffs.speed = Some(Timer::from_seconds(speed.duration, TimerMode::Once));
+                },
+            )
+        });
+    })
+}
+
+fn haste_activation(haste: Haste) -> ActivatedAbilityActivation {
+    Box::new(move |commands: &mut Commands, source_id: PlayerId| {
+        commands.add(move |world: &mut World| {
+            world.run_system_once(
+                move |entity_map: Res<EntityMap>, mut buffses: Query<&mut Buffs>| {
+                    let Some(entity_id) = entity_map.0.get(&source_id.0) else {
+                        return;
+                    };
+                    let Ok(mut buffs) = buffses.get_mut(*entity_id) else {
+                        return;
+                    };
+                    buffs.haste = Some(Timer::from_seconds(haste.duration, TimerMode::Once));
+                },
+            )
+        });
+    })
+}
+
+fn focus_activation(focus: Focus) -> ActivatedAbilityActivation {
+    Box::new(move |commands: &mut Commands, source_id: PlayerId| {
+        commands.add(move |world: &mut World| {
+            world.run_system_once(
+                move |entity_map: Res<EntityMap>, mut buffses: Query<&mut Buffs>| {
+                    let Some(entity_id) = entity_map.0.get(&source_id.0) else {
+                        return;
+                    };
+                    let Ok(mut buffs) = buffses.get_mut(*entity_id) else {
+                        return;
+                    };
+                    buffs.haste = Some(Timer::from_seconds(focus.duration, TimerMode::Once));
+                    buffs.speed = Some(Timer::from_seconds(focus.duration, TimerMode::Once));
+                },
+            )
+        });
+    })
+}
+
 #[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
 pub enum DirectionalAbility {
     Dash(Dash),
diff --git a/src/shared/buffs.rs b/src/shared/buffs.rs
index 4fe033d..e8992d5 100644
--- a/src/shared/buffs.rs
+++ b/src/shared/buffs.rs
@@ -2,6 +2,7 @@ use crate::shared::*;
 
 #[derive(Clone, Component, Default, Debug)]
 pub struct Buffs {
+    pub haste: Option<Timer>,
     pub slow: Option<Timer>,
     pub speed: Option<Timer>,
 }
diff --git a/src/shared/champion.rs b/src/shared/champion.rs
index 05f13ef..da63664 100644
--- a/src/shared/champion.rs
+++ b/src/shared/champion.rs
@@ -33,13 +33,15 @@ impl Champion {
         match self {
             Champion::Meele => BaseStats(Stats {
                 attack_range: 25.,
-                movement_speed: 75.,
+                attack_speed: 0.75,
                 max_health: 150.,
+                movement_speed: 75.,
             }),
             Champion::Ranged => BaseStats(Stats {
                 attack_range: 60.,
-                movement_speed: 85.,
+                attack_speed: 1.5,
                 max_health: 100.,
+                movement_speed: 85.,
             }),
         }
     }
@@ -62,14 +64,17 @@ impl Champion {
                 _ => Ability::Targeted(TargetedAbility::MeeleAttack(MeeleAttack { damage: 5. })),
             },
             Champion::Ranged => match ability_slot {
-                AbilitySlot::Q => Ability::Directional(DirectionalAbility::Spear(Spear {
+                AbilitySlot::E => Ability::Directional(DirectionalAbility::Spear(Spear {
                     max_distance: 250.,
-                    damage: 15.,
+                    damage: 25.,
                 })),
+                AbilitySlot::R => {
+                    Ability::Activated(ActivatedAbility::Focus(Focus { duration: 5. }))
+                }
                 AbilitySlot::G => {
                     Ability::Activated(ActivatedAbility::Speed(Speed { duration: 2.5 }))
                 }
-                _ => Ability::Targeted(TargetedAbility::RangedAttack(RangedAttack { damage: 6. })),
+                _ => Ability::Targeted(TargetedAbility::RangedAttack(RangedAttack { damage: 8. })),
             },
         }
     }
@@ -77,10 +82,10 @@ impl Champion {
     pub fn base_cooldown(self) -> BaseCooldown {
         match self {
             Champion::Meele => {
-                BaseCooldown([0.75, 5., 5., 10., 25., 50., 50.].map(Duration::from_secs_f32))
+                BaseCooldown([0., 5., 5., 10., 25., 50., 50.].map(Duration::from_secs_f32))
             }
             Champion::Ranged => {
-                BaseCooldown([1.25, 5., 5., 10., 25., 50., 50.].map(Duration::from_secs_f32))
+                BaseCooldown([0., 10., 10., 15., 35., 50., 50.].map(Duration::from_secs_f32))
             }
         }
     }
diff --git a/src/shared/cooldown.rs b/src/shared/cooldown.rs
index 8f72520..6995941 100644
--- a/src/shared/cooldown.rs
+++ b/src/shared/cooldown.rs
@@ -4,6 +4,9 @@ use serde::Deserialize;
 use serde::Serialize;
 use std::default::Default;
 
+// TODO `AbilitySlot::A`'s cooldown is unused
+//
+// cf. `Stats`' `attack_speed`
 #[derive(Component, Message, Serialize, Deserialize, Clone, Copy, PartialEq, Debug, Default)]
 pub struct Cooldown(pub [Duration; 7]);
 
diff --git a/src/shared/stats.rs b/src/shared/stats.rs
index bc9a509..239405e 100644
--- a/src/shared/stats.rs
+++ b/src/shared/stats.rs
@@ -3,8 +3,9 @@ use crate::shared::*;
 #[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Debug)]
 pub struct Stats {
     pub attack_range: f32,
-    pub movement_speed: f32,
+    pub attack_speed: f32,
     pub max_health: f32,
+    pub movement_speed: f32,
 }
 
 #[derive(Component, Message, Clone, Copy, Serialize, Deserialize, PartialEq, Debug)]
-- 
cgit v1.2.3