From 7f962b034f888135ce618e03bea40a08b57a68c7 Mon Sep 17 00:00:00 2001
From: Alexander Foremny <aforemny@posteo.de>
Date: Fri, 22 Mar 2024 15:53:45 +0100
Subject: feat: nexuses

---
 src/server.rs          | 182 +++++++++++++++++++++++++++++++++++++++----------
 src/shared.rs          |   2 +
 src/shared/champion.rs |  14 ++++
 src/shared/faction.rs  |  16 +++++
 src/shared/minion.rs   |   7 +-
 src/shared/nexus.rs    |  77 +++++++++++++++++++++
 src/shared/shape.rs    |   4 ++
 src/shared/tower.rs    |   7 +-
 8 files changed, 270 insertions(+), 39 deletions(-)
 create mode 100644 src/shared/faction.rs
 create mode 100644 src/shared/nexus.rs

(limited to 'src')

diff --git a/src/server.rs b/src/server.rs
index fa761fa..b09e96b 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -7,11 +7,13 @@ use crate::shared::area_of_effect::*;
 use crate::shared::buffs::*;
 use crate::shared::champion::*;
 use crate::shared::cooldown::*;
+use crate::shared::faction::*;
 use crate::shared::health::*;
 use crate::shared::health_event::*;
 use crate::shared::immovable::*;
 use crate::shared::imperative::*;
 use crate::shared::minion::*;
+use crate::shared::nexus::*;
 use crate::shared::player::*;
 use crate::shared::projectile::*;
 use crate::shared::shape::*;
@@ -94,42 +96,66 @@ impl Plugin for ServerPlugin {
             .add_systems(FixedUpdate, player_input)
             .add_systems(
                 FixedUpdate,
-                (tower_ai, minion_ai).before(imperative_attack_approach),
+                (tower_ai, (minion_ai_attack, minion_ai_walk).chain())
+                    .before(imperative_attack_approach),
             )
-            .add_systems(FixedUpdate, minion_despawn);
+            .add_systems(FixedUpdate, minion_despawn)
+            .add_systems(FixedUpdate, (nexus_tick, nexus_spawn_minions));
     }
 }
 
 fn setup(mut commands: Commands, mut entity_map: ResMut<EntityMap>) {
     {
-        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 client_id = 1;
+        let entity = commands.spawn(TowerBundle::new(
+            client_id,
+            Vec2::new(0., 100.),
+            Faction::Blue,
+        ));
+        entity_map.0.insert(client_id, entity.id());
     }
     {
-        let entity_id = 2;
+        let client_id = 2;
         let entity = commands.spawn(TowerBundle::new(
-            entity_id,
+            client_id,
             Vec2::new(0., -100.),
-            Color::BLUE,
+            Faction::Red,
         ));
-        entity_map.0.insert(entity_id, entity.id());
+        entity_map.0.insert(client_id, entity.id());
     }
-    for entity_id in 3..=8 {
+    for client_id in 3..=8 {
         let entity = commands.spawn(MinionBundle::new(
-            entity_id,
-            Vec2::new((entity_id - 2) as f32 * 25., 0.),
-            Color::BLUE,
+            client_id,
+            Vec2::new((client_id - 2) as f32 * 25., 0.),
+            Faction::Blue,
         ));
-        entity_map.0.insert(entity_id, entity.id());
+        entity_map.0.insert(client_id, entity.id());
     }
-    for entity_id in 9..=14 {
+    for client_id in 9..=14 {
         let entity = commands.spawn(MinionBundle::new(
-            entity_id,
-            Vec2::new((entity_id - 8) as f32 * -25., 0.),
-            Color::RED,
+            client_id,
+            Vec2::new((client_id - 8) as f32 * -25., 0.),
+            Faction::Red,
         ));
-        entity_map.0.insert(entity_id, entity.id());
+        entity_map.0.insert(client_id, entity.id());
+    }
+    {
+        let client_id = 15;
+        let entity = commands.spawn(NexusBundle::new(
+            client_id,
+            Vec2::new(100., 100.0),
+            Faction::Blue,
+        ));
+        entity_map.0.insert(client_id, entity.id());
+    }
+    {
+        let client_id = 16;
+        let entity = commands.spawn(NexusBundle::new(
+            client_id,
+            Vec2::new(-100., -100.0),
+            Faction::Red,
+        ));
+        entity_map.0.insert(client_id, entity.id());
     }
 }
 
@@ -698,14 +724,26 @@ fn tower_ai(
         &mut Imperative,
         &PlayerPosition,
         &EffectiveStats,
+        &Faction,
     )>,
-    targets: Query<(&PlayerId, &PlayerPosition, &Shape), Without<Tower>>,
+    targets: Query<(&PlayerId, &PlayerPosition, &Shape, &Faction), Without<Tower>>,
 ) {
-    for (mut tower_tower, mut tower_imperative, tower_player_position, tower_effective_stats) in
-        towers.iter_mut()
+    for (
+        mut tower_tower,
+        mut tower_imperative,
+        tower_player_position,
+        tower_effective_stats,
+        tower_faction,
+    ) in towers.iter_mut()
     {
         let mut closest_target = None;
-        for (target_player_id, target_player_position, target_shape) in targets.iter() {
+        for (target_player_id, target_player_position, target_shape, target_faction) in
+            targets.iter()
+        {
+            if target_faction == tower_faction {
+                continue;
+            }
+
             let target_in_range = tower_player_position.0.distance(target_player_position.0)
                 < tower_effective_stats.0.attack_range + target_shape.radius;
 
@@ -737,19 +775,46 @@ fn tower_ai(
     }
 }
 
-fn minion_ai(
-    mut minions: Query<
-        (&PlayerId, &mut Imperative, &PlayerPosition, &EffectiveStats),
-        With<Minion>,
-    >,
-    targets: Query<(&PlayerId, &PlayerPosition, &Shape)>,
+fn minion_ai_walk(
+    mut minions: Query<(&mut Imperative, &PlayerPosition, &Faction), With<Minion>>,
+    nexuses: Query<(&PlayerPosition, &Faction), With<Nexus>>,
 ) {
-    for (minion_player_id, mut minion_imperative, minion_player_position, minion_effective_stats) in
+    for (mut minion_imperative, minion_player_position, minion_faction) in minions.iter_mut() {
+        if *minion_imperative != Imperative::Idle {
+            continue;
+        }
+        let mut closest_target = None;
+        for (nexus_player_position, nexus_faction) in nexuses.iter() {
+            if nexus_faction == minion_faction {
+                continue;
+            }
+            let target_distance = minion_player_position.0.distance(nexus_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((nexus_player_position.0, target_distance));
+            }
+        }
+        let Some((target_player_position, _)) = closest_target else {
+            *minion_imperative = Imperative::Idle;
+            continue;
+        };
+        *minion_imperative = Imperative::WalkTo(target_player_position);
+    }
+}
+fn minion_ai_attack(
+    mut minions: Query<(&mut Imperative, &PlayerPosition, &EffectiveStats, &Faction), With<Minion>>,
+    targets: Query<(&PlayerId, &PlayerPosition, &Shape, &Faction)>,
+) {
+    for (mut minion_imperative, minion_player_position, minion_effective_stats, minion_faction) 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 {
+        for (target_player_id, target_player_position, target_shape, target_faction) in
+            targets.iter()
+        {
+            if target_faction == minion_faction {
                 continue;
             }
             let target_in_range = minion_player_position.0.distance(target_player_position.0)
@@ -773,10 +838,57 @@ fn minion_ai(
     }
 }
 
-fn minion_despawn(minions: Query<(Entity, &Health), With<Minion>>, mut commands: Commands) {
-    for (minion_entity, minion_health) in minions.iter() {
+fn minion_despawn(
+    minions: Query<(Entity, &PlayerId, &Health), With<Minion>>,
+    mut commands: Commands,
+    mut entity_map: ResMut<EntityMap>,
+) {
+    for (minion_entity, minion_player_id, minion_health) in minions.iter() {
         if minion_health.health <= 0. {
-            commands.entity(minion_entity).despawn()
+            commands.entity(minion_entity).despawn();
+            entity_map.0.remove(&minion_player_id.0);
+        }
+    }
+}
+
+fn nexus_spawn_minions(
+    nexuses: Query<(&Nexus, &PlayerPosition, &Faction)>,
+    mut commands: Commands,
+    mut entity_map: ResMut<EntityMap>,
+) {
+    for (nexus_nexus, nexus_player_position, nexus_faction) in nexuses.iter() {
+        if nexus_nexus.spawn_minions.just_finished() {
+            let client_ids = generate_client_ids(5, &entity_map);
+            for client_id in client_ids {
+                let entity = commands.spawn(MinionBundle::new(
+                    client_id,
+                    nexus_player_position.0,
+                    *nexus_faction,
+                ));
+                entity_map.0.insert(client_id, entity.id());
+            }
+        }
+    }
+}
+
+fn generate_client_ids(n: u64, entity_map: &ResMut<EntityMap>) -> Vec<u64> {
+    let mut rng = rand::thread_rng();
+    let mut k = 0;
+    let mut client_ids = vec![];
+    while k < n {
+        let client_id = rng.gen();
+        if entity_map.0.contains_key(&client_id) {
+            continue;
         }
+        client_ids.push(client_id);
+        k += 1;
+    }
+    client_ids
+}
+
+fn nexus_tick(mut nexuses: Query<&mut Nexus>, time: Res<Time>) {
+    let dt = time.delta();
+    for mut nexus in nexuses.iter_mut() {
+        nexus.spawn_minions.tick(dt);
     }
 }
diff --git a/src/shared.rs b/src/shared.rs
index 629ad7b..a4ed8fc 100644
--- a/src/shared.rs
+++ b/src/shared.rs
@@ -15,11 +15,13 @@ pub mod area_of_effect;
 pub mod buffs;
 pub mod champion;
 pub mod cooldown;
+pub mod faction;
 pub mod health;
 pub mod health_event;
 pub mod immovable;
 pub mod imperative;
 pub mod minion;
+pub mod nexus;
 pub mod player;
 pub mod projectile;
 pub mod shape;
diff --git a/src/shared/champion.rs b/src/shared/champion.rs
index 711e520..64fc32e 100644
--- a/src/shared/champion.rs
+++ b/src/shared/champion.rs
@@ -11,6 +11,7 @@ pub enum Champion {
     Ranged,
     Tower,
     Minion,
+    Nexus,
 }
 
 impl Default for Champion {
@@ -28,6 +29,7 @@ impl FromStr for Champion {
             "meele" => Ok(Champion::Meele),
             "tower" => Ok(Champion::Tower),
             "minion" => Ok(Champion::Minion),
+            "nexus" => Ok(Champion::Nexus),
             _ => Err(format!("unknown champion: {}", s)),
         }
     }
@@ -60,6 +62,12 @@ impl Champion {
                 max_health: 50.,
                 movement_speed: 60.,
             }),
+            Champion::Nexus => BaseStats(Stats {
+                attack_range: 0.,
+                attack_speed: 0.,
+                max_health: 2000.,
+                movement_speed: 0.,
+            }),
         }
     }
 
@@ -113,6 +121,9 @@ impl Champion {
             Champion::Minion => match ability_slot {
                 _ => Ability::Targeted(TargetedAbility::RangedAttack(RangedAttack { damage: 2. })),
             },
+            Champion::Nexus => match ability_slot {
+                _ => Ability::Targeted(TargetedAbility::MeeleAttack(MeeleAttack { damage: 0. })),
+            },
         }
     }
 
@@ -130,6 +141,9 @@ impl Champion {
             Champion::Minion => {
                 BaseCooldown([1., 1., 1., 1., 1., 1., 1.].map(Duration::from_secs_f32))
             }
+            Champion::Nexus => {
+                BaseCooldown([0., 0., 0., 0., 0., 0., 0.].map(Duration::from_secs_f32))
+            }
         }
     }
 }
diff --git a/src/shared/faction.rs b/src/shared/faction.rs
new file mode 100644
index 0000000..4ff4a02
--- /dev/null
+++ b/src/shared/faction.rs
@@ -0,0 +1,16 @@
+use crate::shared::*;
+
+#[derive(Component, Clone, Copy, PartialEq, Eq)]
+pub enum Faction {
+    Red,
+    Blue,
+}
+
+impl Faction {
+    pub fn to_color(self) -> Color {
+        match self {
+            Faction::Red => Color::RED,
+            Faction::Blue => Color::BLUE,
+        }
+    }
+}
diff --git a/src/shared/minion.rs b/src/shared/minion.rs
index 5d41f7a..4aa824e 100644
--- a/src/shared/minion.rs
+++ b/src/shared/minion.rs
@@ -1,5 +1,6 @@
 use crate::shared::activation::*;
 use crate::shared::buffs::*;
+use crate::shared::faction::*;
 use crate::shared::player::*;
 use crate::shared::shape::*;
 use crate::shared::stats::*;
@@ -19,11 +20,12 @@ pub struct MinionBundle {
     activation: Activation,
     shape: Shape,
     minion: Minion,
+    faction: Faction,
     replicate: Replicate,
 }
 
 impl MinionBundle {
-    pub fn new(id: ClientId, position: Vec2, color: Color) -> Self {
+    pub fn new(id: ClientId, position: Vec2, faction: Faction) -> Self {
         let mut replicate = Replicate {
             replication_group: ReplicationGroup::default().set_priority(10.),
             ..Default::default()
@@ -38,7 +40,7 @@ impl MinionBundle {
         MinionBundle {
             id: PlayerId(id),
             position: PlayerPosition(position),
-            color: PlayerColor(color),
+            color: PlayerColor(faction.to_color()),
             imperative: Imperative::Idle,
             cooldown: Cooldown::default(),
             health: Health {
@@ -51,6 +53,7 @@ impl MinionBundle {
             activation: Activation::default(),
             shape: Shape::minion(),
             minion: Minion,
+            faction,
             replicate,
         }
     }
diff --git a/src/shared/nexus.rs b/src/shared/nexus.rs
new file mode 100644
index 0000000..df91db9
--- /dev/null
+++ b/src/shared/nexus.rs
@@ -0,0 +1,77 @@
+use crate::shared::activation::*;
+use crate::shared::buffs::*;
+use crate::shared::faction::*;
+use crate::shared::immovable::*;
+use crate::shared::player::*;
+use crate::shared::shape::*;
+use crate::shared::stats::*;
+use crate::shared::*;
+use bevy::utils::Duration;
+
+#[derive(Bundle)]
+pub struct NexusBundle {
+    id: PlayerId,
+    position: PlayerPosition,
+    color: PlayerColor,
+    imperative: Imperative,
+    cooldown: Cooldown,
+    health: Health,
+    champion: Champion,
+    effective_stats: EffectiveStats,
+    buffs: Buffs,
+    activation: Activation,
+    shape: Shape,
+    nexus: Nexus,
+    immovable: Immovable,
+    faction: Faction,
+    replicate: Replicate,
+}
+
+impl NexusBundle {
+    pub fn new(id: ClientId, position: Vec2, faction: Faction) -> 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::Nexus;
+        let effective_stats = EffectiveStats(champion.base_stats().0);
+        NexusBundle {
+            id: PlayerId(id),
+            position: PlayerPosition(position),
+            color: PlayerColor(faction.to_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::nexus(),
+            nexus: Nexus::default(),
+            immovable: Immovable,
+            faction,
+            replicate,
+        }
+    }
+}
+
+#[derive(Component)]
+pub struct Nexus {
+    pub spawn_minions: Timer,
+}
+
+impl Default for Nexus {
+    fn default() -> Self {
+        let mut spawn_minions = Timer::from_seconds(60., TimerMode::Repeating);
+        spawn_minions.set_elapsed(Duration::from_secs_f32(60.));
+        Nexus { spawn_minions }
+    }
+}
diff --git a/src/shared/shape.rs b/src/shared/shape.rs
index 6e11c56..d77403d 100644
--- a/src/shared/shape.rs
+++ b/src/shared/shape.rs
@@ -17,4 +17,8 @@ impl Shape {
     pub fn minion() -> Self {
         Shape { radius: 5. }
     }
+
+    pub fn nexus() -> Self {
+        Shape { radius: 35. }
+    }
 }
diff --git a/src/shared/tower.rs b/src/shared/tower.rs
index 5b62b7a..a21048a 100644
--- a/src/shared/tower.rs
+++ b/src/shared/tower.rs
@@ -1,5 +1,6 @@
 use crate::shared::activation::*;
 use crate::shared::buffs::*;
+use crate::shared::faction::*;
 use crate::shared::immovable::*;
 use crate::shared::player::*;
 use crate::shared::shape::*;
@@ -21,11 +22,12 @@ pub struct TowerBundle {
     shape: Shape,
     tower: Tower,
     immovable: Immovable,
+    faction: Faction,
     replicate: Replicate,
 }
 
 impl TowerBundle {
-    pub fn new(id: ClientId, position: Vec2, color: Color) -> Self {
+    pub fn new(id: ClientId, position: Vec2, faction: Faction) -> Self {
         let mut replicate = Replicate {
             replication_group: ReplicationGroup::default().set_priority(10.),
             ..Default::default()
@@ -40,7 +42,7 @@ impl TowerBundle {
         TowerBundle {
             id: PlayerId(id),
             position: PlayerPosition(position),
-            color: PlayerColor(color),
+            color: PlayerColor(faction.to_color()),
             imperative: Imperative::Idle,
             cooldown: Cooldown::default(),
             health: Health {
@@ -54,6 +56,7 @@ impl TowerBundle {
             shape: Shape::tower(),
             tower: Tower::default(),
             immovable: Immovable,
+            faction,
             replicate,
         }
     }
-- 
cgit v1.2.3