From 2d8740b1e29fc356fbe88cea21059ec83d8c0cf0 Mon Sep 17 00:00:00 2001
From: Alexander Foremny <aforemny@posteo.de>
Date: Fri, 22 Mar 2024 15:42:19 +0100
Subject: feat: minions

---
 src/server.rs          | 87 ++++++++++++++++++++++++++++++++++++++++++++++----
 src/shared.rs          |  1 +
 src/shared/champion.rs | 14 ++++++++
 src/shared/minion.rs   | 60 ++++++++++++++++++++++++++++++++++
 src/shared/shape.rs    |  4 +++
 5 files changed, 159 insertions(+), 7 deletions(-)
 create mode 100644 src/shared/minion.rs

(limited to 'src')

diff --git a/src/server.rs b/src/server.rs
index 4da2253..fa761fa 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -11,6 +11,7 @@ use crate::shared::health::*;
 use crate::shared::health_event::*;
 use crate::shared::immovable::*;
 use crate::shared::imperative::*;
+use crate::shared::minion::*;
 use crate::shared::player::*;
 use crate::shared::projectile::*;
 use crate::shared::shape::*;
@@ -91,17 +92,45 @@ impl Plugin for ServerPlugin {
             .add_systems(FixedUpdate, cooldown_decrement)
             .add_systems(FixedUpdate, (buffs_despawn, buffs_tick).chain())
             .add_systems(FixedUpdate, player_input)
-            .add_systems(FixedUpdate, tower_ai);
+            .add_systems(
+                FixedUpdate,
+                (tower_ai, minion_ai).before(imperative_attack_approach),
+            )
+            .add_systems(FixedUpdate, minion_despawn);
     }
 }
 
 fn setup(mut commands: Commands, mut entity_map: ResMut<EntityMap>) {
-    let entity = commands.spawn(PlayerBundle::new(1, Vec2::ZERO, Color::GRAY));
-    entity_map.0.insert(1, entity.id());
-    let entity = commands.spawn(TowerBundle::new(2, Vec2::new(0., 100.), Color::RED));
-    entity_map.0.insert(2, entity.id());
-    let entity = commands.spawn(TowerBundle::new(3, Vec2::new(0., -100.), Color::BLUE));
-    entity_map.0.insert(3, entity.id());
+    {
+        let entity_id = 1;
+        let entity = commands.spawn(TowerBundle::new(entity_id, Vec2::new(0., 100.), Color::RED));
+        entity_map.0.insert(entity_id, entity.id());
+    }
+    {
+        let entity_id = 2;
+        let entity = commands.spawn(TowerBundle::new(
+            entity_id,
+            Vec2::new(0., -100.),
+            Color::BLUE,
+        ));
+        entity_map.0.insert(entity_id, entity.id());
+    }
+    for entity_id in 3..=8 {
+        let entity = commands.spawn(MinionBundle::new(
+            entity_id,
+            Vec2::new((entity_id - 2) as f32 * 25., 0.),
+            Color::BLUE,
+        ));
+        entity_map.0.insert(entity_id, entity.id());
+    }
+    for entity_id in 9..=14 {
+        let entity = commands.spawn(MinionBundle::new(
+            entity_id,
+            Vec2::new((entity_id - 8) as f32 * -25., 0.),
+            Color::RED,
+        ));
+        entity_map.0.insert(entity_id, entity.id());
+    }
 }
 
 fn connects(
@@ -707,3 +736,47 @@ fn tower_ai(
         tower_tower.last_target_player_id = Some(*target_player_id);
     }
 }
+
+fn minion_ai(
+    mut minions: Query<
+        (&PlayerId, &mut Imperative, &PlayerPosition, &EffectiveStats),
+        With<Minion>,
+    >,
+    targets: Query<(&PlayerId, &PlayerPosition, &Shape)>,
+) {
+    for (minion_player_id, mut minion_imperative, minion_player_position, minion_effective_stats) in
+        minions.iter_mut()
+    {
+        let mut closest_target = None;
+        for (target_player_id, target_player_position, target_shape) in targets.iter() {
+            if target_player_id == minion_player_id {
+                continue;
+            }
+            let target_in_range = minion_player_position.0.distance(target_player_position.0)
+                < minion_effective_stats.0.attack_range + target_shape.radius;
+            if !target_in_range {
+                continue;
+            }
+            let target_distance = minion_player_position.0.distance(target_player_position.0);
+            let target_is_closer = closest_target
+                .map(|(_, closest_target_distance)| target_distance < closest_target_distance)
+                .unwrap_or(true);
+            if target_is_closer {
+                closest_target = Some((target_player_id, target_distance));
+            }
+        }
+        let Some((target_player_id, _)) = closest_target else {
+            *minion_imperative = Imperative::Idle;
+            continue;
+        };
+        *minion_imperative = Imperative::AttackTarget(AbilitySlot::A, *target_player_id);
+    }
+}
+
+fn minion_despawn(minions: Query<(Entity, &Health), With<Minion>>, mut commands: Commands) {
+    for (minion_entity, minion_health) in minions.iter() {
+        if minion_health.health <= 0. {
+            commands.entity(minion_entity).despawn()
+        }
+    }
+}
diff --git a/src/shared.rs b/src/shared.rs
index 3310c8c..629ad7b 100644
--- a/src/shared.rs
+++ b/src/shared.rs
@@ -19,6 +19,7 @@ pub mod health;
 pub mod health_event;
 pub mod immovable;
 pub mod imperative;
+pub mod minion;
 pub mod player;
 pub mod projectile;
 pub mod shape;
diff --git a/src/shared/champion.rs b/src/shared/champion.rs
index 1667672..711e520 100644
--- a/src/shared/champion.rs
+++ b/src/shared/champion.rs
@@ -10,6 +10,7 @@ pub enum Champion {
     Meele,
     Ranged,
     Tower,
+    Minion,
 }
 
 impl Default for Champion {
@@ -26,6 +27,7 @@ impl FromStr for Champion {
             "ranged" => Ok(Champion::Ranged),
             "meele" => Ok(Champion::Meele),
             "tower" => Ok(Champion::Tower),
+            "minion" => Ok(Champion::Minion),
             _ => Err(format!("unknown champion: {}", s)),
         }
     }
@@ -52,6 +54,12 @@ impl Champion {
                 max_health: 500.,
                 movement_speed: 0.,
             }),
+            Champion::Minion => BaseStats(Stats {
+                attack_range: 30.,
+                attack_speed: 1.,
+                max_health: 50.,
+                movement_speed: 60.,
+            }),
         }
     }
 
@@ -102,6 +110,9 @@ impl Champion {
                     Ability::Targeted(TargetedAbility::RangedAttack(RangedAttack { damage: 100. }))
                 }
             },
+            Champion::Minion => match ability_slot {
+                _ => Ability::Targeted(TargetedAbility::RangedAttack(RangedAttack { damage: 2. })),
+            },
         }
     }
 
@@ -116,6 +127,9 @@ impl Champion {
             Champion::Tower => {
                 BaseCooldown([10., 10., 10., 10., 10., 10., 10.].map(Duration::from_secs_f32))
             }
+            Champion::Minion => {
+                BaseCooldown([1., 1., 1., 1., 1., 1., 1.].map(Duration::from_secs_f32))
+            }
         }
     }
 }
diff --git a/src/shared/minion.rs b/src/shared/minion.rs
new file mode 100644
index 0000000..5d41f7a
--- /dev/null
+++ b/src/shared/minion.rs
@@ -0,0 +1,60 @@
+use crate::shared::activation::*;
+use crate::shared::buffs::*;
+use crate::shared::player::*;
+use crate::shared::shape::*;
+use crate::shared::stats::*;
+use crate::shared::*;
+
+#[derive(Bundle)]
+pub struct MinionBundle {
+    id: PlayerId,
+    position: PlayerPosition,
+    color: PlayerColor,
+    imperative: Imperative,
+    cooldown: Cooldown,
+    health: Health,
+    champion: Champion,
+    effective_stats: EffectiveStats,
+    buffs: Buffs,
+    activation: Activation,
+    shape: Shape,
+    minion: Minion,
+    replicate: Replicate,
+}
+
+impl MinionBundle {
+    pub fn new(id: ClientId, position: Vec2, color: Color) -> Self {
+        let mut replicate = Replicate {
+            replication_group: ReplicationGroup::default().set_priority(10.),
+            ..Default::default()
+        };
+        replicate.enable_replicate_once::<PlayerId>();
+        replicate.enable_replicate_once::<PlayerColor>();
+        replicate.target::<Champion>(NetworkTarget::Single(id));
+        replicate.target::<Cooldown>(NetworkTarget::Single(id));
+        replicate.target::<EffectiveStats>(NetworkTarget::Single(id));
+        let champion = Champion::Minion;
+        let effective_stats = EffectiveStats(champion.base_stats().0);
+        MinionBundle {
+            id: PlayerId(id),
+            position: PlayerPosition(position),
+            color: PlayerColor(color),
+            imperative: Imperative::Idle,
+            cooldown: Cooldown::default(),
+            health: Health {
+                health: effective_stats.0.max_health,
+                shield: 0.,
+            },
+            champion,
+            effective_stats,
+            buffs: Buffs::default(),
+            activation: Activation::default(),
+            shape: Shape::minion(),
+            minion: Minion,
+            replicate,
+        }
+    }
+}
+
+#[derive(Component, Message, Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
+pub struct Minion;
diff --git a/src/shared/shape.rs b/src/shared/shape.rs
index 7423375..6e11c56 100644
--- a/src/shared/shape.rs
+++ b/src/shared/shape.rs
@@ -13,4 +13,8 @@ impl Shape {
     pub fn tower() -> Self {
         Shape { radius: 25. }
     }
+
+    pub fn minion() -> Self {
+        Shape { radius: 5. }
+    }
 }
-- 
cgit v1.2.3