From 2d21d709901c96b05d7f0169dd9d1207436c658c Mon Sep 17 00:00:00 2001
From: Alexander Foremny <aforemny@posteo.de>
Date: Tue, 19 Mar 2024 14:13:16 +0100
Subject: feat: area of effect

---
 src/client.rs                | 27 +++++++++++++++++++
 src/protocol.rs              |  2 ++
 src/server.rs                | 54 ++++++++++++++++++++++++++++++++++++-
 src/shared.rs                |  1 +
 src/shared/ability.rs        | 12 ++++++++-
 src/shared/area_of_effect.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 158 insertions(+), 2 deletions(-)
 create mode 100644 src/shared/area_of_effect.rs

(limited to 'src')

diff --git a/src/client.rs b/src/client.rs
index 8da77dc..7e0cebf 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -2,6 +2,7 @@ use crate::client::network::*;
 use crate::protocol::*;
 use crate::shared::ability::*;
 use crate::shared::activation::*;
+use crate::shared::area_of_effect::*;
 use crate::shared::champion::*;
 use crate::shared::cooldown::*;
 use crate::shared::health::*;
@@ -76,6 +77,7 @@ impl Plugin for ClientPlugin {
                 (
                     render_players.after(move_players),
                     render_projectiles.after(move_projectiles),
+                    render_area_of_effects,
                     render_health,
                 ),
             )
@@ -226,6 +228,31 @@ fn render_projectiles(
     }
 }
 
+fn render_area_of_effects(
+    mut commands: Commands,
+    mut materials: ResMut<Assets<ColorMaterial>>,
+    mut meshes: ResMut<Assets<Mesh>>,
+    area_of_effects: Query<(Entity, &AreaOfEffect), Added<AreaOfEffect>>,
+) {
+    for (entity, area_of_effect) in area_of_effects.iter() {
+        if area_of_effect.duration.is_none() {
+            continue;
+        }
+        commands.entity(entity).insert(MaterialMesh2dBundle {
+            mesh: Mesh2dHandle(meshes.add(Circle {
+                radius: area_of_effect.radius,
+            })),
+            material: materials.add(Color::BLACK),
+            transform: Transform::from_xyz(
+                area_of_effect.position.x,
+                area_of_effect.position.y,
+                1.,
+            ),
+            ..Default::default()
+        });
+    }
+}
+
 fn move_projectiles(mut projectiles: Query<(&mut Transform, &Projectile), Changed<Projectile>>) {
     for (mut transform, projectile) in projectiles.iter_mut() {
         let Some(position) = (match projectile.type_ {
diff --git a/src/protocol.rs b/src/protocol.rs
index 8a83260..646974c 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -1,4 +1,5 @@
 use crate::shared::activation::*;
+use crate::shared::area_of_effect::*;
 use crate::shared::champion::*;
 use crate::shared::cooldown::*;
 use crate::shared::health::*;
@@ -42,6 +43,7 @@ pub enum Components {
     Health(Health),
     Champion(Champion),
     EffectiveStats(EffectiveStats),
+    AreaOfEffect(AreaOfEffect),
 }
 
 #[derive(Channel)]
diff --git a/src/server.rs b/src/server.rs
index fc44120..4593cfa 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -3,6 +3,7 @@ use crate::server::entity_map::*;
 use crate::server::network::*;
 use crate::shared::ability::*;
 use crate::shared::activation::*;
+use crate::shared::area_of_effect::*;
 use crate::shared::buffs::*;
 use crate::shared::champion::*;
 use crate::shared::cooldown::*;
@@ -71,8 +72,17 @@ impl Plugin for ServerPlugin {
             .add_systems(
                 FixedUpdate,
                 (
-                    projectile_move.after(imperative_walk_to),
                     projectile_despawn,
+                    projectile_move.after(imperative_walk_to),
+                )
+                    .chain(),
+            )
+            .add_systems(
+                FixedUpdate,
+                (
+                    area_of_effect_despawn,
+                    area_of_effect_tick.after(imperative_walk_to),
+                    area_of_effect_activate,
                 )
                     .chain(),
             )
@@ -580,3 +590,45 @@ fn buffs_despawn(mut buffses: Query<&mut Buffs>) {
         }
     }
 }
+
+fn area_of_effect_tick(mut area_of_effects: Query<&mut AreaOfEffect>, time: Res<Time>) {
+    let dt = time.delta();
+    for mut area_of_effect in area_of_effects.iter_mut() {
+        if let Some(ref mut duration) = area_of_effect.duration {
+            *duration = (*duration - dt.as_secs_f32()).max(0.);
+        } else {
+            area_of_effect.duration = Some(0.);
+        }
+    }
+}
+
+fn area_of_effect_despawn(area_of_effects: Query<(Entity, &AreaOfEffect)>, mut commands: Commands) {
+    for (entity, area_of_effect) in area_of_effects.iter() {
+        let Some(duration) = area_of_effect.duration else {
+            continue;
+        };
+        if duration > 0. {
+            continue;
+        };
+        commands.entity(entity).despawn();
+    }
+}
+
+fn area_of_effect_activate(
+    players: Query<(&PlayerId, &PlayerPosition)>,
+    area_of_effects: Query<&AreaOfEffect>,
+    mut commands: Commands,
+) {
+    for area_of_effect in area_of_effects.iter() {
+        for (player_id, player_position) in players.iter() {
+            if *player_id == area_of_effect.source_player {
+                continue;
+            }
+            if area_of_effect.position.distance(player_position.0)
+                < area_of_effect.radius + PLAYER_RADIUS
+            {
+                area_of_effect.activate()(&mut commands, area_of_effect.source_player, *player_id);
+            }
+        }
+    }
+}
diff --git a/src/shared.rs b/src/shared.rs
index 678c592..8ec056d 100644
--- a/src/shared.rs
+++ b/src/shared.rs
@@ -11,6 +11,7 @@ use std::default::Default;
 
 pub mod ability;
 pub mod activation;
+pub mod area_of_effect;
 pub mod buffs;
 pub mod champion;
 pub mod cooldown;
diff --git a/src/shared/ability.rs b/src/shared/ability.rs
index 80c74b2..2cd44c4 100644
--- a/src/shared/ability.rs
+++ b/src/shared/ability.rs
@@ -1,4 +1,5 @@
 use crate::server::entity_map::*;
+use crate::shared::area_of_effect::*;
 use crate::shared::buffs::*;
 use crate::shared::player::*;
 use crate::shared::projectile::*;
@@ -220,7 +221,8 @@ fn dash_activation(dash: Dash) -> DirectionalAbilityActivation {
                           mut set: ParamSet<(
                         Query<&mut PlayerPosition>,
                         Query<(&PlayerId, &PlayerPosition)>,
-                    )>| {
+                    )>,
+                          mut commands: Commands| {
                         let Some(source_entity) = ({
                             let mut source_entity = None;
                             for (entity, player_id) in players.iter() {
@@ -261,6 +263,14 @@ fn dash_activation(dash: Dash) -> DirectionalAbilityActivation {
                         if let Ok(mut position) = positions.get_mut(source_entity) {
                             position.0 = dash_end;
                         }
+
+                        commands.spawn(AreaOfEffectBundle::new(AreaOfEffect {
+                            position: dash_end,
+                            radius: 1.5 * PLAYER_RADIUS,
+                            duration: None,
+                            source_player,
+                            area_of_effect_type: AreaOfEffectType::Slow,
+                        }));
                     },
                 )
             });
diff --git a/src/shared/area_of_effect.rs b/src/shared/area_of_effect.rs
new file mode 100644
index 0000000..dcdc86c
--- /dev/null
+++ b/src/shared/area_of_effect.rs
@@ -0,0 +1,64 @@
+use crate::server::entity_map::*;
+use crate::shared::buffs::*;
+use crate::shared::player::*;
+use crate::shared::*;
+use bevy::ecs::system::*;
+
+#[derive(Bundle)]
+pub struct AreaOfEffectBundle {
+    area_of_effect: AreaOfEffect,
+    replicate: Replicate,
+}
+
+impl AreaOfEffectBundle {
+    pub fn new(area_of_effect: AreaOfEffect) -> Self {
+        AreaOfEffectBundle {
+            area_of_effect,
+            replicate: Replicate::default(),
+        }
+    }
+}
+
+#[derive(Component, Message, Serialize, Deserialize, Clone, PartialEq, Debug)]
+pub struct AreaOfEffect {
+    pub position: Vec2,
+    pub radius: f32,
+    // `duration = None` means `AreaOfEffect` exists only for a single server tick
+    pub duration: Option<f32>,
+    pub source_player: PlayerId,
+    pub area_of_effect_type: AreaOfEffectType,
+}
+
+#[derive(Component, Message, Serialize, Deserialize, Clone, PartialEq, Debug)]
+pub enum AreaOfEffectType {
+    Slow,
+}
+
+pub type AreaOfEffectActivation = Box<dyn FnOnce(&mut Commands, PlayerId, PlayerId) -> ()>;
+
+impl AreaOfEffect {
+    pub fn activate(&self) -> AreaOfEffectActivation {
+        match self.area_of_effect_type {
+            AreaOfEffectType::Slow => Box::new(
+                move |commands: &mut Commands,
+                      _source_player_id: PlayerId,
+                      target_player_id: PlayerId| {
+                    commands.add(move |world: &mut World| {
+                        world.run_system_once(
+                            move |entity_map: Res<EntityMap>, mut buffs: Query<&mut Buffs>| {
+                                let Some(target_entity) = entity_map.0.get(&target_player_id.0)
+                                else {
+                                    return;
+                                };
+                                let Ok(mut buffs) = buffs.get_mut(*target_entity) else {
+                                    return;
+                                };
+                                buffs.slow = Some(0.75);
+                            },
+                        )
+                    })
+                },
+            ),
+        }
+    }
+}
-- 
cgit v1.2.3