C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Plugins;
using ProtoBuf;
using Rust;
using Rust.Ai;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random;
namespace Oxide.Plugins {
[Info("ZombieHorde", "k1lly0u", "0.4.8")]
class ZombieHorde : RustPlugin {
[PluginReference]
private Plugin Kits, Spawns;
private static ZombieHorde Instance {
get;
set;
}
public enum SpawnSystem {
None, Random, SpawnsDatabase
}
public enum SpawnState {
Spawn, Despawn
}
private static BaseNavigator.NavigationSpeed DefaultRoamSpeed;
private const string ADMIN_PERMISSION = "zombiehorde.admin";
private const string IGNORE_PERMISSION = "zombiehorde.ignore";
#region Oxide Hooks
private void OnServerInitialized() {
Instance = this;
permission.RegisterPermission(ADMIN_PERMISSION, this);
permission.RegisterPermission(IGNORE_PERMISSION, this);
if (!Configuration.Member.TargetedByPeaceKeeperTurrets) Unsubscribe(nameof(CanEntityBeHostile));
if (Configuration.Member.TargetedByAPC) Unsubscribe(nameof(CanBradleyApcTarget));
DefaultRoamSpeed = ParseType<BaseNavigator.NavigationSpeed>(Configuration.Horde.DefaultRoamSpeed);
ValidateLoadoutProfiles();
ValidateSpawnSystem();
CreateMonumentHordeOrders();
}
private void OnEntityTakeDamage(BaseCombatEntity baseCombatEntity, HitInfo hitInfo) {
if (hitInfo != null) {
if (hitInfo.InitiatorPlayer is ZombieNPC) {
if (hitInfo.damageTypes.Get(DamageType.Explosion) > 0) {
hitInfo.damageTypes.ScaleAll(ConVar.Halloween.scarecrow_beancan_vs_player_dmg_modifier);
return;
}
float damageMultiplier = (hitInfo.InitiatorPlayer as ZombieNPC).Loadout.DamageMultiplier;
if (damageMultiplier != 1f) hitInfo.damageTypes.ScaleAll(damageMultiplier);
}
}
}
private void OnPlayerDeath(BasePlayer player, HitInfo hitInfo) {
if (player == null || hitInfo == null) return;
if (player is ZombieNPC) {
(player as ZombieNPC).Horde.OnMemberKilled((player as ZombieNPC), hitInfo.Initiator);
return;
}
if (Configuration.Horde.CreateOnDeath && hitInfo.InitiatorPlayer is ZombieNPC) (hitInfo.InitiatorPlayer as ZombieNPC).Horde.OnPlayerKilled(player);
}
private void OnEntityKill(ZombieNPC zombieNPC) {
if (zombieNPC != null && zombieNPC.Horde != null && !zombieNPC.Horde.isDespawning) zombieNPC.Horde.OnMemberKilled(zombieNPC, null);
}
private object CanBeTargeted(ZombieNPC zombieNPC, GunTrap gunTrap) => Configuration.Member.TargetedByTurrets ? null : (object)false;
private object CanBeTargeted(ZombieNPC zombieNPC, FlameTurret flameTurret) => Configuration.Member.TargetedByTurrets ? null : (object)false;
private object CanBeTargeted(ZombieNPC zombieNPC, AutoTurret autoTurret) {
if (Configuration.Member.TargetedByTurrets) return null;
if ((autoTurret.PeacekeeperMode() || autoTurret is NPCAutoTurret) && Configuration.Member.TargetedByPeaceKeeperTurrets) return null;
return false;
}
private object CanEntityBeHostile(ZombieNPC zombieNPC) => true;
private object CanBradleyApcTarget(BradleyAPC bradleyAPC, ZombieNPC zombieNPC) => false;
private object OnNpcTarget(NPCPlayer npcPlayer, ZombieNPC zombieNPC) => Configuration.Member.TargetedByNPCs ? null : (object)true;
private object OnNpcTarget(BaseNpc baseNpc, ZombieNPC zombieNPC) => Configuration.Member.TargetedByAnimals ? null : (object)true;
private void Unload() {
Horde.SpawnOrder.OnUnload();
for (int i = Horde.AllHordes.Count - 1; i >= 0; i--) Horde.AllHordes[i].Destroy(true, true);
Horde.AllHordes.Clear();
ZombieNPC[] zombies = UnityEngine.Object.FindObjectsOfType<ZombieNPC>();
for (int i = 0; i < zombies?.Length; i++) zombies[i].Kill(BaseNetworkable.DestroyMode.None);
Configuration = null;
Instance = null;
}
#endregion
#region Sensations
private void OnEntityKill(TimedExplosive timedExplosive) {
if (!Configuration.Horde.UseSenses) return;
Sense.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = timedExplosive.transform.position, Radius = 80f,
}
);
}
private void OnEntityKill(TreeEntity treeEntity) {
if (!Configuration.Horde.UseSenses) return;
Sense.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = treeEntity.transform.position, Radius = 30f,
}
);
}
private void OnEntityKill(OreResourceEntity oreResourceEntity) {
if (!Configuration.Horde.UseSenses) return;
Sense.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = oreResourceEntity.transform.position, Radius = 30f,
}
);
}
private void OnDispenserGather(ResourceDispenser dispenser, BaseEntity entity, Item item) {
if (!Configuration.Horde.UseSenses) return;
Sense.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = dispenser.transform.position, Radius = 20f
}
);
}
#endregion
#region Functions
private T ParseType<T>(string type) {
try {
return (T)Enum.Parse(typeof(T), type, true);
}
catch {
return default(T);
}
}
#region Horde Spawning
private List<Vector3> _spawnPoints;
private SpawnSystem _spawnSystem = SpawnSystem.None;
private const int SPAWN_RAYCAST_MASK = 1 << 0 | 1 << 8 | 1 << 15 | 1 << 17 | 1 << 21 | 1 << 29;
private const TerrainTopology.Enum SPAWN_TOPOLOGY_MASK = (TerrainTopology.Enum.Ocean | TerrainTopology.Enum.River | TerrainTopology.Enum.Lake | TerrainTopology.Enum.Cliff | TerrainTopology.Enum.Cliffside | TerrainTopology.Enum.Offshore | TerrainTopology.Enum.Summit | TerrainTopology.Enum.Decor);
private static bool ContainsTopologyAtPoint(TerrainTopology.Enum mask, Vector3 position) => (TerrainMeta.TopologyMap.GetTopology(position, 1f) & (int)mask) != 0;
private bool ValidateSpawnSystem() {
_spawnSystem = ParseType<SpawnSystem>(Configuration.Horde.SpawnType);
if (_spawnSystem == SpawnSystem.None) {
PrintError("You have set an invalid value in the config entry \"Spawn Type\".Unable to spawn hordes!");
return false;
}
else if (_spawnSystem == SpawnSystem.SpawnsDatabase) {
if (Spawns != null) {
if (string.IsNullOrEmpty(Configuration.Horde.SpawnFile)) {
PrintError("You have selected SpawnsDatabase as your method of spawning hordes, however you have not specified a spawn file.Unable to spawn hordes!");
return false;
}
object success = Spawns?.Call("LoadSpawnFile", Configuration.Horde.SpawnFile);
if (success is List<Vector3>) {
_spawnPoints = success as List<Vector3>;
if (_spawnPoints.Count > 0) return true;
}
PrintError("You have selected SpawnsDatabase as your method of spawning hordes, however the spawn file you have chosen is either invalid, or has no spawn points.Unable to spawn hordes!");
return false;
}
else PrintError("You have selected SpawnsDatabase as your method of spawning hordes, however SpawnsDatabase is not loaded on your server.Unable to spawn hordes!");
return false;
}
return true;
}
private Vector3 GetSpawnPoint() {
switch (_spawnSystem) {
case SpawnSystem.None : break;
case SpawnSystem.SpawnsDatabase : {
if (Spawns == null) {
PrintError("Tried getting a spawn point but SpawnsDatabase is null.Make sure SpawnsDatabase is still loaded to continue using custom spawn points");
break;
}
if (_spawnPoints == null || _spawnPoints.Count == 0) {
PrintError("No spawnpoints have been loaded from the designated spawnfile.Defaulting to Rust spawns");
break;
}
Vector3 spawnPoint = _spawnPoints.GetRandom();
_spawnPoints.Remove(spawnPoint);
if (_spawnPoints.Count == 0) _spawnPoints = (List<Vector3>)Spawns.Call("LoadSpawnFile", Configuration.Horde.SpawnFile);
return spawnPoint;
}
}
float size = (World.Size / 2f) * 0.75f;
for (int i = 0; i < 10; i++) {
Vector2 randomInCircle = Random.insideUnitCircle * size;
Vector3 position = new Vector3(randomInCircle.x, 0, randomInCircle.y);
position.y = TerrainMeta.HeightMap.GetHeight(position);
if (NavmeshSpawnPoint.Find(position, 25f, out position)) {
if (Physics.SphereCast(new Ray(position + (Vector3.up * 5f), Vector3.down), 10f, 10f, SPAWN_RAYCAST_MASK)) continue;
if (ContainsTopologyAtPoint(SPAWN_TOPOLOGY_MASK, position)) continue;
if (WaterLevel.GetWaterDepth(position, true, null) <= 0.01f) return position;
}
}
return ServerMgr.FindSpawnPoint().pos;
}
private void CreateMonumentHordeOrders() {
int count = 0;
GameObject[] allobjects = UnityEngine.Object.FindObjectsOfType<GameObject>();
foreach (GameObject gobject in allobjects) {
if (count >= Configuration.Horde.MaximumHordes) break;
if (gobject.name.Contains("autospawn/monument")) {
Transform tr = gobject.transform;
Vector3 position = tr.position;
if (position == Vector3.zero) continue;
if (gobject.name.Contains("powerplant_1") && Configuration.Monument.Powerplant.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-30.8f, 0.2f, -15.8f)), Configuration.Monument.Powerplant);
count++;
continue;
}
if (gobject.name.Contains("military_tunnel_1") && Configuration.Monument.Tunnels.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-7.4f, 13.4f, 53.8f)), Configuration.Monument.Tunnels);
count++;
continue;
}
if (gobject.name.Contains("harbor_1") && Configuration.Monument.LargeHarbor.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(54.7f, 5.1f, -39.6f)), Configuration.Monument.LargeHarbor);
count++;
continue;
}
if (gobject.name.Contains("harbor_2") && Configuration.Monument.SmallHarbor.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-66.6f, 4.9f, 16.2f)), Configuration.Monument.SmallHarbor);
count++;
continue;
}
if (gobject.name.Contains("airfield_1") && Configuration.Monument.Airfield.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-12.4f, 0.2f, -28.9f)), Configuration.Monument.Airfield);
count++;
continue;
}
if (gobject.name.Contains("trainyard_1") && Configuration.Monument.Trainyard.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(35.8f, 0.2f, -0.8f)), Configuration.Monument.Trainyard);
count++;
continue;
}
if (gobject.name.Contains("water_treatment_plant_1") && Configuration.Monument.WaterTreatment.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(11.1f, 0.3f, -80.2f)), Configuration.Monument.WaterTreatment);
count++;
continue;
}
if (gobject.name.Contains("warehouse") && Configuration.Monument.Warehouse.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(16.6f, 0.1f, -7.5f)), Configuration.Monument.Warehouse);
count++;
continue;
}
if (gobject.name.Contains("satellite_dish") && Configuration.Monument.Satellite.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(18.6f, 6.0f, -7.5f)), Configuration.Monument.Satellite);
count++;
continue;
}
if (gobject.name.Contains("sphere_tank") && Configuration.Monument.Dome.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-44.6f, 5.8f, -3.0f)), Configuration.Monument.Dome);
count++;
continue;
}
if (gobject.name.Contains("radtown_small_3") && Configuration.Monument.Radtown.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-16.3f, -2.1f, -3.3f)), Configuration.Monument.Radtown);
count++;
continue;
}
if (gobject.name.Contains("launch_site_1") && Configuration.Monument.LaunchSite.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(222.1f, 3.3f, 0.0f)), Configuration.Monument.LaunchSite);
count++;
continue;
}
if (gobject.name.Contains("gas_station_1") && Configuration.Monument.GasStation.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-9.8f, 3.0f, 7.2f)), Configuration.Monument.GasStation);
count++;
continue;
}
if (gobject.name.Contains("supermarket_1") && Configuration.Monument.Supermarket.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(5.5f, 0.0f, -20.5f)), Configuration.Monument.Supermarket);
count++;
continue;
}
if (gobject.name.Contains("mining_quarry_c") && Configuration.Monument.HQMQuarry.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(15.8f, 4.5f, -1.5f)), Configuration.Monument.HQMQuarry);
count++;
continue;
}
if (gobject.name.Contains("mining_quarry_a") && Configuration.Monument.SulfurQuarry.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-0.8f, 0.6f, 11.4f)), Configuration.Monument.SulfurQuarry);
count++;
continue;
}
if (gobject.name.Contains("mining_quarry_b") && Configuration.Monument.StoneQuarry.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-7.6f, 0.2f, 12.3f)), Configuration.Monument.StoneQuarry);
count++;
continue;
}
if (gobject.name.Contains("junkyard_1") && Configuration.Monument.Junkyard.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-16.7f, 0.2f, 1.4f)), Configuration.Monument.Junkyard);
count++;
continue;
}
}
}
foreach(ConfigData.MonumentSpawn.CustomSpawnPoints customSpawnPoint in Configuration.Monument.Custom) {
if (customSpawnPoint.Enabled) {
Horde.SpawnOrder.Create(customSpawnPoint.Location, Configuration.Horde.InitialMemberCount, customSpawnPoint.HordeSize, customSpawnPoint.RoamDistance, customSpawnPoint.Profile);
count++;
}
}
if (count < Configuration.Horde.MaximumHordes) CreateRandomHordes();
}
private void CreateRandomHordes() {
int amountToCreate = Configuration.Horde.MaximumHordes - Horde.AllHordes.Count;
for (int i = 0; i < amountToCreate; i++) {
float roamDistance = Configuration.Horde.LocalRoam ? Configuration.Horde.RoamDistance : -1;
string profile = Configuration.Horde.UseProfiles ? Configuration.HordeProfiles.Keys.ToArray().GetRandom() : string.Empty;
Horde.SpawnOrder.Create(GetSpawnPoint(), Configuration.Horde.InitialMemberCount, Configuration.Horde.MaximumMemberCount, roamDistance, profile);
}
}
#endregion
#region Loadouts
private static LootContainer.LootSpawnSlot[] _defaultLootSpawns;
private static PlayerInventoryProperties[] _defaultLoadouts;
private static LootContainer.LootSpawnSlot[] DefaultLootSpawns {
get {
if (_defaultLootSpawns == null) {
ScarecrowNPC scarecrowNPC = GameManager.server.FindPrefab("assets/prefabs/npc/scarecrow/scarecrow.prefab").GetComponent<ScarecrowNPC>();
_defaultLootSpawns = scarecrowNPC.LootSpawnSlots;
_defaultLoadouts = scarecrowNPC.loadouts;
}
return _defaultLootSpawns;
}
}
private static PlayerInventoryProperties[] DefaultLoadouts {
get {
if (_defaultLoadouts == null) {
ScarecrowNPC scarecrowNPC = GameManager.server.FindPrefab("assets/prefabs/npc/scarecrow/scarecrow.prefab").GetComponent<ScarecrowNPC>();
_defaultLootSpawns = scarecrowNPC.LootSpawnSlots;
_defaultLoadouts = scarecrowNPC.loadouts;
}
return _defaultLoadouts;
}
}
private void ValidateLoadoutProfiles() {
Puts("Validating horde profiles...");
bool hasChanged = false;
for (int i = Configuration.HordeProfiles.Count - 1; i >= 0; i--) {
string key = Configuration.HordeProfiles.ElementAt(i).Key;
for (int y = Configuration.HordeProfiles[key].Count - 1; y >= 0; y--) {
string loadoutId = Configuration.HordeProfiles[key][y];
if (!Configuration.Member.Loadouts.Any(x => x.LoadoutID == loadoutId)) {
Puts($"Loadout profile {loadoutId} does not exist.Removing from config");
Configuration.HordeProfiles[key].Remove(loadoutId);
hasChanged = true;
}
}
if (Configuration.HordeProfiles[key].Count <= 0) {
Puts($"Horde profile {key} does not have any valid loadouts.Removing from config");
Configuration.HordeProfiles.Remove(key);
hasChanged = true;
}
}
if (hasChanged) SaveConfig();
}
#endregion
#region Spawning
private static ZombieNPC _reference;
private static ZombieNPC Reference {
get {
if (_reference == null) {
const string SCIENTIST_PREFAB = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_heavy.prefab";
GameObject gameObject = Instantiate.GameObject(GameManager.server.FindPrefab(SCIENTIST_PREFAB), Vector3.zero, Quaternion.identity);
gameObject.name = SCIENTIST_PREFAB;
gameObject.SetActive(false);
ScientistNPC scientistNPC = gameObject.GetComponent<ScientistNPC>();
ScientistBrain scientistBrain = gameObject.GetComponent<ScientistBrain>();
NPCPlayerNavigator scientistNavigator = gameObject.GetComponent<NPCPlayerNavigator>();
ZombieNPC zombieNPC = gameObject.AddComponent<ZombieNPC>();
ZombieNPCBrain zombieNPCBrain = gameObject.AddComponent<ZombieNPCBrain>();
ZombieNavigator zombieNavigator = gameObject.AddComponent<ZombieNavigator>();
CopySerializeableFields<NPCPlayer>(scientistNPC, zombieNPC);
CopySerializeableFields<NPCPlayerNavigator>(scientistNavigator, zombieNavigator);
zombieNPC.enableSaving = false;
zombieNPCBrain.UseQueuedMovementUpdates = scientistBrain.UseQueuedMovementUpdates;
zombieNPCBrain.AllowedToSleep = Configuration.Member.EnableDormantSystem;
zombieNPCBrain.DefaultDesignSO = scientistBrain.DefaultDesignSO;
zombieNPCBrain.Designs = new List<AIDesignSO>(scientistBrain.Designs);
zombieNPCBrain.InstanceSpecificDesign = scientistBrain.InstanceSpecificDesign;
zombieNPCBrain.CheckLOS = scientistBrain.CheckLOS;
zombieNPCBrain.UseAIDesign = true;
zombieNPCBrain.Pet = false;
scientistBrain._baseEntity = scientistNPC;
UnityEngine.Object.DestroyImmediate(scientistNavigator);
UnityEngine.Object.DestroyImmediate(scientistBrain, true);
UnityEngine.Object.DestroyImmediate(scientistNPC, true);
_reference = zombieNPC;
}
return _reference;
}
}
private static ZombieNPC InstantiateEntity(Vector3 position) {
ZombieNPC zombieNPC = UnityEngine.Object.Instantiate<ZombieNPC>(Reference, position, Quaternion.identity);
zombieNPC.name = Reference.name;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(zombieNPC.gameObject, Rust.Server.EntityScene);
zombieNPC.gameObject.SetActive(true);
return zombieNPC;
}
private static void CopySerializeableFields<T>(T src, T dst) {
FieldInfo[] srcFields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (FieldInfo field in srcFields) {
object value = field.GetValue(src);
field.SetValue(dst, value);
}
}
private static class NavmeshSpawnPoint {
private static NavMeshHit navmeshHit;
private static RaycastHit raycastHit;
private static readonly Collider[] _buffer = new Collider[256];
private const int WORLD_LAYER = 65536;
public static bool Find(Vector3 targetPosition, float maxDistance, out Vector3 position) {
for (int i = 0; i < 10; i++) {
position = i == 0 ? targetPosition : targetPosition + (Random.onUnitSphere * maxDistance);
if (NavMesh.SamplePosition(position, out navmeshHit, maxDistance, 1)) {
if (IsInRockPrefab(navmeshHit.position)) continue;
if (IsNearWorldCollider(navmeshHit.position)) continue;
if (navmeshHit.position.y < TerrainMeta.WaterMap.GetHeight(navmeshHit.position)) continue;
position = navmeshHit.position;
return true;
}
}
position = default(Vector3);
return false;
}
private static bool IsInRockPrefab(Vector3 position) {
Physics.queriesHitBackfaces = true;
bool isInRock = Physics.Raycast(position, Vector3.up, out raycastHit, 20f, WORLD_LAYER, QueryTriggerInteraction.Ignore) && BLOCKED_COLLIDERS.Any(s => raycastHit.collider?.gameObject?.name.Contains(s, CompareOptions.OrdinalIgnoreCase) ?? false);
Physics.queriesHitBackfaces = false;
return isInRock;
}
private static bool IsNearWorldCollider(Vector3 position) {
Physics.queriesHitBackfaces = true;
int count = Physics.OverlapSphereNonAlloc(position, 2f, _buffer, WORLD_LAYER, QueryTriggerInteraction.Ignore);
Physics.queriesHitBackfaces = false;
int removed = 0;
for (int i = 0; i < count; i++) {
if (ACCEPTED_COLLIDERS.Any(s => _buffer[i].gameObject.name.Contains(s, CompareOptions.OrdinalIgnoreCase))) removed++;
}
return count - removed > 0;
}
private static readonly string[] ACCEPTED_COLLIDERS = new string[] {
"road", "carpark", "rocket_factory", "range", "train_track", "runway", "_grounds", "concrete_slabs", "lighthouse", "cave", "office", "walkways", "sphere", "tunnel", "industrial", "junkyard"
}
;
private static readonly string[] BLOCKED_COLLIDERS = new string[] {
"rock", "cliff", "junk", "range", "invisible"
}
;
}
#endregion
#endregion
public class Horde {
public static List<Horde> AllHordes = new List<Horde>();
private static readonly Spatial.Grid<Horde> HordeGrid = new Spatial.Grid<Horde>(32, 8096f);
private static readonly Horde[] HordeGridQueryResults = new Horde[4];
private static readonly BasePlayer[] PlayerVicinityQueryResults = new BasePlayer[32];
public List<ZombieNPC> members;
//public bool DebugMode = false;
public readonly Vector3 InitialPosition;
public readonly bool IsLocalHorde;
public readonly float MaximumRoamDistance;
private readonly int initialMemberCount;
private readonly int maximumMemberCount;
private readonly string hordeProfile;
private float nextUpdateTime;
private float nextSeperationCheckTime;
private float nextGrowthTime;
private float nextMergeTime;
private float nextSleepTime;
internal bool isDespawning;
private const float HORDE_UPDATE_RATE = 1f;
private const float SEPERATION_CHECK_RATE = 10f;
private const float MERGE_CHECK_RATE = 10f;
private const float SLEEP_CHECK_RATE = 5f;
public ZombieNPC Leader {
get;
private set;
}
public bool IsSleeping {
get;
private set;
}
public Vector3 CentralLocation {
get;
private set;
}
public bool HordeOnAlert {
get;
private set;
}
public int MemberCount => members.Count;
public static bool Create(SpawnOrder spawnOrder) {
Horde horde = new Horde(spawnOrder);
for (int i = 0; i < spawnOrder.InitialMemberCount; i++) horde.SpawnMember(spawnOrder.Position);
if (horde.members.Count == 0) {
horde.Destroy();
return false;
}
AllHordes.Add(horde);
horde.CentralLocation = horde.CalculateCentralLocation();
HordeGrid.Add(horde, horde.CentralLocation.x, horde.CentralLocation.z);
return true;
}
public Horde(SpawnOrder spawnOrder) {
members = Pool.GetList<ZombieNPC>();
InitialPosition = CentralLocation = spawnOrder.Position;
IsLocalHorde = spawnOrder.MaximumRoamDistance > 0;
MaximumRoamDistance = spawnOrder.MaximumRoamDistance;
initialMemberCount = spawnOrder.InitialMemberCount;
maximumMemberCount = spawnOrder.MaximumMemberCount;
hordeProfile = spawnOrder.HordeProfile;
nextSeperationCheckTime = Time.time + SEPERATION_CHECK_RATE;
nextGrowthTime = Time.time + Configuration.Horde.GrowthRate;
nextMergeTime = Time.time + MERGE_CHECK_RATE;
nextSleepTime = Time.time + SLEEP_CHECK_RATE + Random.Range(1f, 5f);
}
public void Update() {
if (members == null || members.Count == 0) return;
if (Time.time < nextUpdateTime) return;
nextUpdateTime = Time.time + HORDE_UPDATE_RATE;
CentralLocation = CalculateCentralLocation();
if (Configuration.Member.EnableDormantSystem) DoSleepChecks();
//if (DebugMode)
//{
// Debug.Log($"Horde Update ({CentralLocation}).Sleeping? {IsSleeping}");
// for (int i = 0; i < members.Count; i++)
// {
// ZombieNPC zombieNPC = members[i];
// Debug.Log($"Member #{i} state {zombieNPC.Brain.CurrentState.StateType} (Is Leader? {zombieNPC.IsGroupLeader}");
// }
//}
if (IsSleeping) return;
HordeGrid.Move(this, CentralLocation.x, CentralLocation.z);
TryMergeHordes();
TryGrowHorde();
TryCongregateHorde();
MoveRoamersTowardsTarget();
}
private void MoveRoamersTowardsTarget() {
BaseEntity target;
HordeOnAlert = AnyHasTarget(out target);
if (Leader != null && !Leader.IsDestroyed && target != null && !target.IsDestroyed) SetLeaderRoamTarget(target.transform.position);
}
private void TryCongregateHorde() {
if (Time.time > nextSeperationCheckTime) {
nextSeperationCheckTime = Time.time + SEPERATION_CHECK_RATE;
if (GetLargestSeperation() > 30f) {
//if (DebugMode)
// Debug.Log($"Horde is seperated, attempt re-group.Leader state {Leader.CurrentState}");
if (Leader.CurrentState <= AIState.Roam) SetLeaderRoamTarget(CentralLocation);
for (int i = 0; i < members.Count; i++) members[i].IsAlert = true;
}
}
}
public void RegisterInterestInTarget(ZombieNPC interestedMember, BaseEntity baseEntity) {
if (baseEntity == null || members == null) return;
//if (DebugMode)
// Debug.Log($"Register interest in {baseEntity.ShortPrefabName} ({baseEntity.transform.position}).");
for (int i = 0; i < members.Count; i++) {
ZombieNPC hordeMember = members[i];
if (hordeMember == null || hordeMember.IsDestroyed || interestedMember == hordeMember) continue;
hordeMember.Brain.Senses.Memory.SetKnown(baseEntity, hordeMember, null);
}
if (Leader != null && !Leader.IsDestroyed && !Leader.HasTarget) SetLeaderRoamTarget(baseEntity.transform.position);
}
public bool HasTarget() {
if (members != null) {
for (int i = 0; i < members.Count; i++) {
ZombieNPC hordeMember = members[i];
if (hordeMember.HasTarget) return true;
}
}
return false;
}
public bool AnyHasTarget(out BaseEntity target) {
if (members != null) {
for (int i = 0; i < members.Count; i++) {
ZombieNPC hordeMember = members[i];
if (hordeMember.HasTarget) {
target = hordeMember.CurrentTarget;
return true;
}
}
}
target = null;
return false;
}
public void ResetRoamTarget() {
if (members == null) return;
//if (DebugMode)
// Debug.Log("Reset roam target");
for (int i = 0; i < members.Count; i++) {
ZombieNPC hordeMember = members[i];
if (hordeMember.IsGroupLeader) continue;
hordeMember.ResetRoamState();
}
}
public void SetLeaderRoamTarget(Vector3 position) {
//if (DebugMode)
// Debug.Log($"Set leader roam target {position}");
if (Leader != null && !Leader.IsDestroyed) Leader.SetRoamTargetOverride(position);
ResetRoamTarget();
}
public Vector3 GetLeaderDestination() {
if (Leader == null || Leader.IsDestroyed || Leader.Brain == null || Leader.Brain.Events == null) return CentralLocation;
return Leader.Brain.Navigator.Destination;
}
public void OnMemberKilled(ZombieNPC zombieNPC, BaseEntity initiator) {
if (zombieNPC == null) return;
if (members == null || !members.Contains(zombieNPC)) return;
members.Remove(zombieNPC);
if (members.Count == 0) Destroy();
else {
if (zombieNPC.IsGroupLeader) {
Leader = members.GetRandom();
Leader.IsGroupLeader = true;
}
if ((initiator is BasePlayer && Leader.CanTargetBasePlayer(initiator as BasePlayer)) || (initiator is BaseNpc && Leader.CanTargetEntity(initiator))) RegisterInterestInTarget(null, initiator);
}
}
public void OnPlayerKilled(BasePlayer player) {
if (Configuration.Horde.CreateOnDeath && MemberCount < maximumMemberCount) {
Vector3 position;
if (NavmeshSpawnPoint.Find(player.transform.position, 10f, out position)) SpawnMember(position);
}
}
public void SpawnMember(Vector3 position) {
ZombieNPC zombieNPC = InstantiateEntity(position);
zombieNPC.SetHordeProfile(hordeProfile);
zombieNPC.Horde = this;
zombieNPC.Spawn();
members.Add(zombieNPC);
if (members.Count == 1) {
Leader = zombieNPC;
Leader.IsGroupLeader = true;
}
else zombieNPC.Invoke(zombieNPC.OnInitialSpawn, 1f);
}
public void Destroy(bool permanent = false, bool killNpcs = true) {
isDespawning = true;
if (killNpcs) {
for (int i = members.Count - 1; i >= 0; i--) {
ZombieNPC zombieNPC = members[i];
if (zombieNPC != null && !zombieNPC.IsDestroyed) zombieNPC.Kill();
}
}
members.Clear();
Pool.FreeList(ref members);
HordeGrid.Remove(this);
AllHordes.Remove(this);
if (!permanent && AllHordes.Count <= Configuration.Horde.MaximumHordes) Instance.timer.In(Configuration.Horde.RespawnTime, () => SpawnOrder.Create(this));
}
private Vector3 CalculateCentralLocation() {
Vector3 location = Vector3.zero;
if (members == null || members.Count == 0) return location;
int count = 0;
for (int i = 0; i < members.Count; i++) {
ZombieNPC zombieNPC = members[i];
if (zombieNPC == null || zombieNPC.IsDestroyed) continue;
location += zombieNPC.Transform.position;
count++;
}
return location /= count;
}
private float GetLargestSeperation() {
float distance = 0;
if (members != null) {
for (int i = 0; i < members.Count; i++) {
ZombieNPC zombieNPC = members[i];
if (zombieNPC != null && !zombieNPC.IsDestroyed) {
float d = Vector3.Distance(zombieNPC.Transform.position, CentralLocation);
if (d > distance) distance = d;
}
}
}
return distance;
}
private void TryGrowHorde() {
if (Configuration.Horde.GrowthRate <= 0 || nextGrowthTime < Time.time) {
if (MemberCount < maximumMemberCount) SpawnMember(members.GetRandom().Transform.position);
nextGrowthTime = Time.time + Configuration.Horde.GrowthRate;
}
}
#region Horde Merging
private static bool HordeMergeQuery(Horde horde) => horde.MemberCount < horde.maximumMemberCount;
private void TryMergeHordes() {
if (!Configuration.Horde.MergeHordes || nextMergeTime > Time.time) return;
nextMergeTime = Time.time + MERGE_CHECK_RATE;
if (members == null || MemberCount >= maximumMemberCount) return;
int results = HordeGrid.Query(CentralLocation.x, CentralLocation.z, 30f, HordeGridQueryResults, HordeMergeQuery);
if (results > 1) {
int amountToMerge = maximumMemberCount - members.Count;
for (int i = 0; i < results; i++) {
Horde otherHorde = HordeGridQueryResults[i];
if (otherHorde == this) continue;
if (MemberCount >= maximumMemberCount || otherHorde.members == null) break;
if (amountToMerge >= otherHorde.members.Count) {
for (int y = 0; y < otherHorde.members.Count; y++) {
ZombieNPC zombieNPC = otherHorde.members[y];
members.Add(zombieNPC);
zombieNPC.Horde = this;
zombieNPC.OnInitialSpawn();
}
otherHorde.members.Clear();
otherHorde.Destroy();
}
else {
for (int y = 0; y < amountToMerge; y++) {
if (otherHorde.members.Count > 0) {
ZombieNPC zombieNPC = otherHorde.members[otherHorde.MemberCount - 1];
members.Add(zombieNPC);
zombieNPC.Horde = this;
zombieNPC.OnInitialSpawn();
otherHorde.members.Remove(zombieNPC);
}
}
}
}
}
}
#endregion
#region Sleeping
private void DoSleepChecks() {
if (Time.time >= nextSleepTime) {
nextSleepTime = Time.time + SLEEP_CHECK_RATE + Random.Range(1f, 5f);
int count = BaseEntity.Query.Server.GetPlayersInSphere(CentralLocation, AiManager.ai_to_player_distance_wakeup_range, PlayerVicinityQueryResults, HordeSleepPlayerFilter);
if (count > 0) {
if (IsSleeping) SetSleeping(false);
}
else {
if (!IsSleeping) SetSleeping(true);
}
}
}
private void SetSleeping(bool sleep) {
if (members == null) return;
for (int i = 0; i < members.Count; i++) {
ZombieNPC zombieNPC = members[i];
if (zombieNPC == null || zombieNPC.Brain == null) continue;
if (zombieNPC.Brain.sleeping == sleep) continue;
zombieNPC.Brain.SetSleeping(sleep);
}
IsSleeping = sleep;
}
public void ForceWakeFromSleep() {
if (!IsSleeping) return;
SetSleeping(false);
}
private static bool HordeSleepPlayerFilter(BaseEntity entity) {
BasePlayer basePlayer = entity as BasePlayer;
if (basePlayer == null || !basePlayer.IsConnected) return false;
if (basePlayer is ZombieNPC) return false;
if (Configuration.Member.IgnoreSleepers && basePlayer.IsSleeping()) return false;
return true;
}
#endregion
public int GetMemberIndex(ZombieNPC zombieNPC) => members.IndexOf(zombieNPC);
public class SpawnOrder {
public Vector3 Position {
get;
private set;
}
public int InitialMemberCount {
get;
private set;
}
public int MaximumMemberCount {
get;
private set;
}
public float MaximumRoamDistance {
get;
private set;
}
public string HordeProfile {
get;
private set;
}
public SpawnOrder(Vector3 position, int initialMemberCount, int maximumMemberCount, float maximumRoamDistance, string hordeProfile) {
this.Position = position;
this.InitialMemberCount = initialMemberCount;
this.MaximumMemberCount = maximumMemberCount;
this.MaximumRoamDistance = maximumRoamDistance;
this.HordeProfile = hordeProfile;
}
#region Static
private static Queue<SpawnOrder> _spawnOrders = new Queue<SpawnOrder>();
private static Coroutine _spawnRoutine;
private static Coroutine _despawnRoutine;
private static bool _isSpawning;
private static bool _isDespawning;
public static SpawnState State;
static SpawnOrder() {
State = Configuration.TimedSpawns.Enabled ? (ShouldSpawn() ? SpawnState.Spawn : SpawnState.Despawn) : SpawnState.Spawn;
if (Configuration.TimedSpawns.Enabled) StartTimer();
}
internal static void Create(Vector3 position, int initialMemberCount, int maximumMemberCount, float maximumRoamDistance, string hordeProfile) {
if (NavmeshSpawnPoint.Find(position, 10f, out position)) {
_spawnOrders.Enqueue(new SpawnOrder(position, initialMemberCount, maximumMemberCount, maximumRoamDistance, hordeProfile));
if (!_isSpawning && State == SpawnState.Spawn) DequeueAndSpawn();
}
}
internal static void Create(Horde horde) {
Vector3 position;
if (NavmeshSpawnPoint.Find(horde.IsLocalHorde ? horde.InitialPosition : Instance.GetSpawnPoint(), 10f, out position)) {
_spawnOrders.Enqueue(new SpawnOrder(position, horde.initialMemberCount, horde.maximumMemberCount, horde.IsLocalHorde ? horde.MaximumRoamDistance : -1, horde.hordeProfile));
if (!_isSpawning && State == SpawnState.Spawn) DequeueAndSpawn();
}
}
internal static void Create(Vector3 position, ConfigData.MonumentSpawn.MonumentSettings settings) {
if (NavmeshSpawnPoint.Find(position, 10f, out position)) {
_spawnOrders.Enqueue(new SpawnOrder(position, Configuration.Horde.InitialMemberCount, settings.HordeSize, settings.RoamDistance, settings.Profile));
if (!_isSpawning && State == SpawnState.Spawn) DequeueAndSpawn();
}
}
private static void DequeueAndSpawn() {
_spawnRoutine = ServerMgr.Instance.StartCoroutine(ProcessSpawnOrders());
}
private static void QueueAndDespawn() {
_despawnRoutine = ServerMgr.Instance.StartCoroutine(ProcessDespawn());
}
internal static void StopSpawning() {
if (_spawnRoutine != null) ServerMgr.Instance.StopCoroutine(_spawnRoutine);
_isSpawning = false;
}
internal static void StopDespawning() {
if (_despawnRoutine != null) ServerMgr.Instance.StopCoroutine(_despawnRoutine);
_isDespawning = false;
}
private static IEnumerator ProcessSpawnOrders() {
if (_spawnOrders.Count == 0) yield break;
_isSpawning = true;
RESTART : if (_isDespawning) StopDespawning();
while (AllHordes.Count > Configuration.Horde.MaximumHordes) yield return CoroutineEx.waitForSeconds(10f);
SpawnOrder spawnOrder = _spawnOrders.Dequeue();
if (spawnOrder != null) Horde.Create(spawnOrder);
if (_spawnOrders.Count > 0) {
yield return CoroutineEx.waitForSeconds(3f);
goto RESTART;
}
_isSpawning = false;
}
private static IEnumerator ProcessDespawn() {
_isDespawning = true;
if (_isSpawning) StopSpawning();
// ... [Code preview truncated due to length. Full source in attached file] ...