C#:
using Facepunch;
using HarmonyLib;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Plugins;
using Oxide.Ext.ChaosNPC;
using Rust;
using Rust.Ai;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Rust.Ai.Gen2;
using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random;
namespace Oxide.Plugins {
[Info("ZombieHorde", "k1lly0u", "0.6.27")]
class ZombieHorde : RustPlugin, IChaosNPCPlugin {
[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));
if (!Configuration.Member.DespawnDudExplosives && !Configuration.Member.ExplodeDudExplosives) Unsubscribe(nameof(OnExplosiveDud));
DefaultRoamSpeed = ParseType<BaseNavigator.NavigationSpeed>(Configuration.Horde.DefaultRoamSpeed);
ValidateLoadoutProfiles();
ValidateSpawnSystem();
Horde.SpawnOrder.InitializeSpawnOrders();
CreateMonumentHordeOrders();
}
protected override void LoadDefaultMessages() => lang.RegisterMessages(new Dictionary<string, string> {
["Notification.BeginSpawn"]
= "<color=#ce422b>CAUTION!</color> Zombies have been spotted in the area", ["Notification.BeginDespawn"] = "<color=#ce422b>CAUTION!</color> The zombies appear to be dispersing"
}
, this);
private void OnEntityTakeDamage(BaseCombatEntity baseCombatEntity, HitInfo hitInfo) {
ZombieNPC zombieNpc = hitInfo?.InitiatorPlayer as ZombieNPC;
if (!zombieNpc || !baseCombatEntity || hitInfo == null) return;
bool hasExplosionDamage = hitInfo.damageTypes.Get(DamageType.Explosion) > 0;
if (baseCombatEntity is BuildingBlock or SimpleBuildingBlock or Door) {
if (hasExplosionDamage) {
if (Configuration.Member.IgnoreBuildingMultiplierNotOwner || Configuration.Member.DisableBuildingMultiplierNotOwner) {
BasePlayer target = zombieNpc.CurrentTarget as BasePlayer;
if (!target) goto SCALE_DAMAGE;
if (baseCombatEntity.OwnerID == target.userID) goto SCALE_DAMAGE;
BuildingPrivlidge buildingPrivilege = (baseCombatEntity as DecayEntity).GetBuildingPrivilege();
if (buildingPrivilege && buildingPrivilege.IsAuthed(target.userID)) goto SCALE_DAMAGE;
if (Configuration.Member.DisableBuildingMultiplierNotOwner) hitInfo.damageTypes.Scale(DamageType.Explosion, 0);
return;
}
SCALE_DAMAGE : hitInfo.damageTypes.Scale(DamageType.Explosion, Configuration.Member.ExplosiveBuildingDamageMultiplier);
return;
}
AttackEntity attackEntity = zombieNpc.GetAttackEntity();
if (attackEntity is BaseMelee) {
hitInfo.damageTypes.ScaleAll(Configuration.Member.MeleeBuildingDamageMultiplier);
return;
}
}
if (hasExplosionDamage) {
if (baseCombatEntity is BasePlayer) {
if ((baseCombatEntity as BasePlayer).IsNpc) goto APPLY_MULTIPLIER;
hitInfo.damageTypes.ScaleAll(ConVar.Halloween.scarecrow_beancan_vs_player_dmg_modifier);
return;
}
hitInfo.damageTypes.ScaleAll(0);
return;
}
APPLY_MULTIPLIER : float damageMultiplier = zombieNpc.Loadout.DamageMultiplier;
if (!Mathf.Approximately(damageMultiplier, 1f)) hitInfo.damageTypes.ScaleAll(damageMultiplier);
}
private object OnExplosiveDud(DudTimedExplosive dudTimedExplosive) {
if (dudTimedExplosive.creatorEntity is ZombieNPC) {
if (Configuration.Member.ExplodeDudExplosives) return false;
NextFrame(() => {
if (dudTimedExplosive && !dudTimedExplosive.IsDestroyed) dudTimedExplosive.KillMessage();
}
);
}
return null;
}
private void OnPlayerDeath(BasePlayer player, HitInfo hitInfo) {
if (player == null || hitInfo == null) return;
if (player is ZombieNPC zombieNpc) {
zombieNpc.Horde.OnMemberKilled(zombieNpc, hitInfo.Initiator);
return;
}
if (Configuration.Horde.CreateOnDeath && hitInfo.InitiatorPlayer is ZombieNPC initiatorNpc) initiatorNpc.Horde.OnPlayerKilled(player);
}
private void OnEntityKill(ZombieNPC zombieNpc) {
if (zombieNpc != null && zombieNpc.Horde is {
isDespawning : false
}
) 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 && !(autoTurret is NPCAutoTurret)) return null;
if ((Configuration.Member.TargetedByPeaceKeeperTurrets || Configuration.Member.TargetedByTurrets) && autoTurret is NPCAutoTurret) {
if (autoTurret.target == null) autoTurret.SetTarget(zombieNpc);
return null;
}
if (autoTurret.PeacekeeperMode() && 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 && !zombieNpc.Brain.sleeping ? null : (object)true;
private object OnNpcTarget(BaseNpc baseNpc, ZombieNPC zombieNpc) => Configuration.Member.TargetedByAnimals && !zombieNpc.Brain.sleeping ? null : (object)true;
private object OnNpcTarget(BaseNPC2 baseNpc, ZombieNPC zombieNpc) => Configuration.Member.TargetedByAnimals && !zombieNpc.Brain.sleeping ? null : (object)true;
private object OnEntityEnter(TriggerSafeZone triggerSafeZone, ZombieNPC zombieNpc) {
if (triggerSafeZone && zombieNpc) {
if (triggerSafeZone.contents != null) triggerSafeZone.contents.Remove(zombieNpc.gameObject);
zombieNpc.LeaveTrigger(triggerSafeZone);
return true;
}
return null;
}
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);
}
}
private static bool IsInOrOnBuilding(BaseEntity baseEntity) {
if (!baseEntity || baseEntity.IsDestroyed) return false;
const int CONSTRUCTION_LAYER = 1 << 21;
Vector3 position = baseEntity.transform.position;
if (Physics.Raycast(position, Vector3.up, out RaycastHit raycastHit, 40f, CONSTRUCTION_LAYER) || Physics.Raycast(position, Vector3.down, out raycastHit, 20f, CONSTRUCTION_LAYER)) {
BaseEntity hitEntity = raycastHit.collider.ToBaseEntity();
return hitEntity is BuildingBlock or SimpleBuildingBlock;
}
return false;
}
private static Quaternion SafeLookRotation(Vector3 forward, Vector3 upwards) {
if (forward == Vector3.zero) return Quaternion.identity;
return Quaternion.LookRotation(forward, upwards);
}
#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 | TerrainTopology.Enum.Monument);
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;
}
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;
}
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, false, 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("ferry_terminal_1") && Configuration.Monument.Ferry.Enabled) {
Horde.SpawnOrder.Create(tr.TransformPoint(new Vector3(-6.9f, 5.3f, 6.2f)), Configuration.Monument.Ferry);
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 && customSpawnPoint.Location.IsValid) {
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 Chaos NPC
public bool InitializeStates(BaseAIBrain customNpcBrain) {
customNpcBrain.AddState(new BaseAIBrain.BaseIdleState());
customNpcBrain.AddState(new ZombieNPCBrain.ZombieChaseState());
customNpcBrain.AddState(new ZombieNPCBrain.ZombieAttackState());
customNpcBrain.AddState(new ZombieNPCBrain.ZombieRoamState());
customNpcBrain.AddState(new ZombieNPCBrain.ZombieMountedState());
return true;
}
public bool WantsToPopulateLoot(CustomScientistNPC customNpc, NPCPlayerCorpse npcplayerCorpse) {
if (Configuration.Loot.DropDefault) return false;
ConfigData.LootTable.RandomLoot randomLoot = Configuration.Loot.Random;
ZombieNPC zombieNpc = customNpc as ZombieNPC;
if (zombieNpc && zombieNpc.Loadout.LootOverride?.List?.Count > 0) randomLoot = zombieNpc.Loadout.LootOverride;
int count = Random.Range(randomLoot.Minimum, randomLoot.Maximum);
int spawnedCount = 0;
int loopCount = 0;
while (true) {
loopCount++;
if (loopCount > 3) goto EndLootSpawn;
float probability = Random.Range(0f, 1f);
List<ConfigData.LootTable.RandomLoot.LootDefinition> definitions = new List<ConfigData.LootTable.RandomLoot.LootDefinition>(randomLoot.List);
for (int i = 0; i < randomLoot.List.Count; i++) {
ConfigData.LootTable.RandomLoot.LootDefinition lootDefinition = definitions.GetRandom();
definitions.Remove(lootDefinition);
if (lootDefinition.Probability >= probability) {
lootDefinition.Create(npcplayerCorpse.containers[0]);
spawnedCount++;
if (spawnedCount >= count) goto EndLootSpawn;
}
}
}
EndLootSpawn : return true;
}
private bool ChaosNPC_PreSpawn(ZombieNPC zombieNpc) => false;
public byte[] GetCustomDesign() => new byte[] {
8, 1, 8, 3, 8, 15, 8, 2, 8, 6, 18, 75, 8, 0, 16, 1, 26, 12, 8, 17, 16, 4, 24, 0, 32, 4, 40, 3, 48, 0, 26, 12, 8, 14, 16, 1, 24, 0, 32, 0, 40, 0, 48, 1, 26, 25, 8, 0, 16, 3, 24, 0, 32, 0, 40, 0, 48, 2, 162, 6, 10, 13, 0, 0, 0, 0, 21, 0, 0, 0, 0, 26, 12, 8, 3, 16, 1, 24, 0, 32, 4, 40, 0, 48, 3, 32, 0, 18, 158, 1, 8, 1, 16, 3, 26, 12, 8, 17, 16, 4, 24, 0, 32, 4, 40, 3, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 1, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 2, 26, 12, 8, 5, 16, 2, 24, 0, 32, 0, 40, 0, 48, 3, 26, 12, 8, 18, 16, 0, 24, 0, 32, 0, 40, 0, 48, 4, 26, 12, 8, 15, 16, 2, 24, 0, 32, 0, 40, 0, 48, 5, 26, 29, 8, 12, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 0, 32, 0, 40, 0, 48, 6, 218, 6, 5, 13, 0, 0, 160, 65, 26, 21, 8, 14, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 0, 32, 0, 40, 0, 48, 7, 26, 12, 8, 20, 16, 0, 24, 0, 32, 0, 40, 0, 48, 8, 32, 0, 18, 113, 8, 2, 16, 15, 26, 12, 8, 17, 16, 4, 24, 0, 32, 4, 40, 3, 48, 0, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 1, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 2, 26, 12, 8, 20, 16, 0, 24, 0, 32, 0, 40, 0, 48, 3, 26, 12, 8, 5, 16, 1, 24, 1, 32, 0, 40, 0, 48, 4, 26, 12, 8, 15, 16, 1, 24, 1, 32, 0, 40, 0, 48, 5, 26, 21, 8, 14, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 0, 32, 0, 40, 0, 48, 6, 32, 0, 18, 62, 8, 3, 16, 2, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 1, 26, 12, 8, 14, 16, 1, 24, 0, 32, 0, 40, 0, 48, 2, 26, 12, 8, 3, 16, 1, 24, 0, 32, 4, 40, 0, 48, 3, 32, 0, 18, 48, 8, 4, 16, 6, 26, 12, 8, 17, 16, 0, 24, 1, 32, 4, 40, 3, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 1, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 2, 32, 0, 24, 0, 34, 13, 90, 111, 109, 98, 105, 101, 32, 68, 101, 115, 105, 103, 110, 40, 0, 48, 0
}
;
#endregion
#region Loadouts
private static Definitions _zombieDefinition;
private static Definitions ZombieDefinition {
get {
if (_zombieDefinition == null) {
ScarecrowNPC scarecrowNpc = GameManager.server.FindPrefab("assets/prefabs/npc/scarecrow/scarecrow.prefab").GetComponent<ScarecrowNPC>();
_zombieDefinition = new Definitions {
Loadouts = scarecrowNpc.loadouts, LootSpawns = scarecrowNpc.LootSpawnSlots
}
;
}
return _zombieDefinition;
}
}
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.All(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
#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 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 float NextThrownWeaponTime {
get;
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.Get<List<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 (IsSleeping) return;
HordeGrid.Move(this, CentralLocation.x, CentralLocation.z);
TryMergeHordes();
TryGrowHorde();
TryCongregateHorde();
MoveRoamersTowardsTarget();
}
private void MoveRoamersTowardsTarget() {
HordeOnAlert = AnyHasTarget(out BaseEntity target);
if (Leader && !Leader.IsDestroyed && target && !target.IsDestroyed) SetLeaderRoamTarget(target.transform.position);
}
private void TryCongregateHorde() {
if (Time.time > nextSeperationCheckTime) {
nextSeperationCheckTime = Time.time + SEPERATION_CHECK_RATE;
if (GetLargestSeperation() > 30f) {
if (Leader.CurrentState <= AIState.Roam) SetLeaderRoamTarget(CentralLocation);
}
}
}
public void RegisterInterestInTarget(ZombieNPC interestedMember, BaseEntity baseEntity, bool force) {
if (baseEntity == null || members == null) return;
for (int i = 0; i < members.Count; i++) {
ZombieNPC hordeMember = members[i];
if (!hordeMember || hordeMember.IsDestroyed || interestedMember == hordeMember) continue;
hordeMember.Brain.Senses.Memory.SetKnown(baseEntity, hordeMember, null);
if (force) {
hordeMember.Brain.Events.Memory.Entity.Set(baseEntity, hordeMember.Brain.Events.CurrentInputMemorySlot);
hordeMember.Brain.SwitchToState(AIState.Chase, 1);
}
}
if (!force && Leader && !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 HasHumanTarget() {
if (members != null) {
for (int i = 0; i < members.Count; i++) {
ZombieNPC hordeMember = members[i];
if (hordeMember.HasHumanTargetInRange) 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;
for (int i = 0; i < members.Count; i++) {
ZombieNPC hordeMember = members[i];
if (hordeMember.IsGroupLeader) continue;
hordeMember.ResetRoamState();
}
}
public void SetLeaderRoamTarget(Vector3 position) {
if (Leader != null && !Leader.IsDestroyed && !Leader.RecentlySetDestination) {
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, zombieNpc.isHidingInside || zombieNpc.unreachableLastFrame);
}
}
public void OnPlayerKilled(BasePlayer player) {
if (Configuration.Horde.CreateOnDeath && MemberCount < maximumMemberCount) {
if (NavmeshSpawnPoint.Find(player.transform.position, 10f, out Vector3 position)) SpawnMember(position);
}
}
public void SpawnMember(Vector3 position) {
ConfigData.MemberOptions.Loadout loadout = null;
if (!string.IsNullOrEmpty(hordeProfile) && Configuration.HordeProfiles.ContainsKey(hordeProfile)) {
string loadoutId = Configuration.HordeProfiles[hordeProfile].GetRandom();
loadout = Configuration.Member.Loadouts.FirstOrDefault(x => x.LoadoutID == loadoutId);
}
if (loadout == null) loadout = Configuration.Member.Loadouts.GetRandom();
ZombieNPC zombieNpc = ChaosNPC.SpawnNPC<ZombieNPC, ZombieNPCBrain>(Instance, position, loadout.NPCSettings);
if (!zombieNpc) return;
zombieNpc.Loadout = loadout;
zombieNpc.Horde = this;
zombieNpc.gameObject.AwakeFromInstantiate();
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 && !zombieNpc.IsDestroyed) zombieNpc.Kill();
}
}
members.Clear();
Pool.FreeUnmanaged(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.IsGroupLeader = false;
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.IsGroupLeader = false;
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 || !zombieNpc.Brain) continue;
if (zombieNpc.Brain.sleeping == sleep) continue;
((ZombieNPCBrain)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 || !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;
public static void InitializeSpawnOrders() {
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();
}
// ... [Code preview truncated due to length. Full source in attached file] ...