using System; using System.Linq; using System.Reflection; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using Oxide.Core; using Oxide.Core.Plugins; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Facepunch; using Rust; using Rust.Ai; using Rust.Ai.Gen2; using HarmonyLib; namespace Oxide.Plugins { [Info("Huntsman", "Krungh Crow", "1.4.4", ResourceId = 316)] [Description("The Hunter hunts you when u kill Game")] #region Changelogs and ToDo /********************************************************************** * v1.0.5 : Fixed Npc triggering huntsman when they kill a animal * v1.0.5 : Fixed error when animals kill animals * v1.0.6 : added the option to spawn a backpack on npc death * v1.0.7 : Fix for huntsman only spawning using even percentage settings * v1.0.8 : Fix for despawn error (not an issue before) changed hurt for destroy * v1.0.9 : Terrainheight improvements(now only for rocks) * : Block Huntsman to spawn inside bases * : Huntsman gun is random when using outfits * : Extra checks for kits * v1.0.10 : Added a name to the hunters dropped backpack inventory panel * v1.1.0 : Patched to ignore ChickenBow Chickens * v1.1.1 : Added extra checks on Huntsman kills * v1.1.2 : Possible fix for occasional NRE * v1.1.3 : Fixed typo in one of the Puts (tryed --> tried) * : Each animal can now be en/disabled to trigger a HuntsMan * : Added a steamid chaticon * v1.1.4 : Disabled targeting of Npc, Animals and Dwellers. * v1.1.5 : Patched for NRE and disabling the attacking of vendor npc * v1.1.6 : Added watercheck and suicide when walking under water. * : Hunters will now ignore sleepers * v1.1.7 : Changed position of Nullcheck in the [OnEntityDeath(global::HumanNPC hunter, HitInfo info)] block * v1.1.8 : Patch * v1.1.9 : Reverted back to last working huntsman kill call * : harvesting the HuntsMan body will give you his skull and flesh * v1.2.0 : Improved spawnheight (spawning inside rocks will still occur but less frequently) * v1.2.1 : Waterheight max is set to 0.6f * : Npc should not float mid air now (spawn inside rocks is minimal) * : Blocked messages from responding if npc did not have a valid spawnspot in most cases * v1.2.2 : HumanNpc should not be targeted by Huntsman now * : NRE fix on animal death * : Performance update * v1.2.3 : Small fixes * : Added support for BasePet (frankenstein) * v1.2.4 : Added support for custom hunter name * v1.2.5 : Blocked npc targeting the huntsman * : Fixed HuntsMan despawn on plugin un/reload * : Added soundeffect in the direction where the hunter is spawned * : Added animal name on animal kill notification * v1.2.6 : Patched for december 2nd rust update * v1.2.7 : Fixed oxide mixing up HumanNPC plugin(same name) and npc type * v1.2.8 : Corrected version nr * v1.3.0 : Fix for NPC attackrange * v1.3.1 : Patched various AI behaviour * : NPC now ignore players in safezone * : Fixed NPC being Idle after spawning * : Switched from junkpile to roam scientists * v1.3.2 : Added support for BetterNpcNames * : Hunters dropped backpack contains the npc name * : Huntsman should now be more fixed to spawnlocation * : Added API : bool IsHuntsmanNpc(global::HumanNPC npc) * object OnEntityDeath(global::HumanNPC npc, HitInfo info) { if (npc == null) return null; if (HuntsMan.Call("IsHuntsmanNpc", npc)) { Puts($"Target : {npc} net.ID : [{npc.net.ID}] Event : HuntsMan NPC"); } return null; } * v1.3.3 : Added API : void OnHuntsmanSpawned(global::HumanNPC npc) * : Added API : void OnHuntsmanKilled(global::HumanNPC npc) * : Added Support for the new Alpha and Omega Animal Types * : Added Support for BotReSpawn npc spawning * : - Forces the name from HuntsMan if spawned with BotReSpawn * : - Using HuntsMan suicide timer (make sure BotReSpawn profile suicidetimer is set high) * : - Backpack can drop from BotReSpawn npc * : Language file changed delete before updating * v1.3.4 : Added slight delay for npc loadout * : Added spawncheck in the npc loadout sequence * : Fix for animal name display * v1.3.5 : Fixed NPC brain call. * v1.3.6 : Patched for 6/7/2023 Rust Update * v1.3.7 : Fix for invis Scientist Suit (Flechen) * Possible fix for weapon equiping * v1.4.0 : Better formatting using npckits code block * Minor tweeks in its behaviour * Can now use custom item names in the loot system * Can now use probability 0-1 in the loot system * Tweeked the lootspawning * v1.4.1 : Polarbear propperly added to the list * v1.4.2 : Added Hook CanSpawnHuntsman(BaseAnimalNPC animal) * Added Hook OnHuntsmanSpawn(global::HumanNPC npc) pre process no return behaviour * Removed Usage of BetterNpcNames * Patched Huntersname due to Rust changes * Fixed not adding npc to the npclist correctly * Fixed backpackdrop * v1.4.4 : Added support for wolf2 (added wolf 2 to the cfg) * * Notes * * For adding the recycler when using the ExtendedRecycler plugin { "probability": 0.6, "shortname": "box.repair.bench", "name": "Recycler", "skin": 1594245394, "amountMin": 1, "amount": 1 }, * **********************************************************************/ #endregion class HuntsMan : RustPlugin { [PluginReference] Plugin BotReSpawn , ChickenBow, Kits; public static Dictionary SpawnedHunters { get; set; } = new Dictionary(); #region Variables private bool CanTrigger = true; private bool Debug; bool BlockSpawn; bool IsSpawned; bool UseBotRespawn; const string hunter = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_roam.prefab"; const ulong chaticon = 76561199188297846; string HunterName; string prefix; private Dictionary> Skins { get; set; } = new Dictionary>(); private readonly List _Hunter = new List(); public static HuntsMan Instance { get; private set; } System.Random Rand; #endregion #region HarmonyPatching private Harmony harmony; public static string sTitle = ""; public static string sNaming = "{Title} {RandomName} {}"; Dictionary StoredNames = new Dictionary(); [HarmonyPatch(typeof(ScientistNPC))] [HarmonyPatch("displayName" , MethodType.Getter)] public static class DisplayName { static bool Prefix(ref string __result , ScientistNPC __instance) { if (__instance != null && instance.StoredNames.ContainsKey(__instance.userID)) { System.Random rd = new System.Random(); string npcname = Instance.HunterName; __result = npcname; return false; } return true; } } #endregion #region Mono public static HuntsMan instance; void OnServerInitialized() { instance = this; } public class Hunters : FacepunchBehaviour { public global::HumanNPC npc; public bool ReturningToHome = false; public Vector3 SpawnPoint; void Start() { npc = GetComponent(); InvokeRepeating("GoHome", 1.0f, 1.0f);//1 Invoke(nameof(_UseBrain), 0.1f);//0.1 } public void _UseBrain() { #region navigation npc.Brain.Navigator.Agent.agentTypeID = -1372625422; npc.Brain.Navigator.DefaultArea = "Walkable"; npc.Brain.Navigator.Agent.autoRepath = true; npc.Brain.Navigator.enabled = true; npc.Brain.Navigator.CanUseNavMesh = true; npc.Brain.Navigator.BestCoverPointMaxDistance = instance.configData.HunterData.HunterRoam / 2;//0 npc.Brain.Navigator.BestRoamPointMaxDistance = instance.configData.HunterData.HunterRoam;//0 npc.Brain.Navigator.MaxRoamDistanceFromHome = instance.configData.HunterData.HunterRoam; npc.Brain.Navigator.Init(npc, npc.Brain.Navigator.Agent); npc.Brain.Navigator.SetDestination(SpawnPoint, BaseNavigator.NavigationSpeed.Slow, 0f, 0f); #endregion #region senses & Targeting npc.Brain.ForceSetAge(0); npc.Brain.AllowedToSleep = false; npc.Brain.sleeping = false; npc.Brain.SenseRange = 30f;//30 npc.Brain.ListenRange = 40f;//40 npc.Brain.Senses.Init(npc, npc.Brain, 5f, 140f, 140f, -1f, true, true, true, 140f, false, false, true, EntityType.Player, false); npc.Brain.TargetLostRange = 25f;//25 npc.Brain.HostileTargetsOnly = false; npc.Brain.IgnoreSafeZonePlayers = true; #endregion } void GoHome() { if (npc == null || npc.IsDestroyed || npc.isMounted) return; if (!npc.HasBrain) return; if (npc.Brain.Senses.Memory.Targets.Count > 0) { for (var i = 0; i < npc.Brain.Senses.Memory.Targets.Count; i++) { BaseEntity target = npc.Brain.Senses.Memory.Targets[i]; BasePlayer player = target as BasePlayer; if (target == null || !player.IsAlive() || target.limitNetworking) { WipeMemory(); ReturningToHome = true; return; } if (npc.Distance(player.transform.position) > 40f) { WipeMemory(); SettargetDestination(SpawnPoint); ReturningToHome = true; return; } } } var distanceHome = Vector3.Distance(npc.transform.position, SpawnPoint); if (ReturningToHome == false) { if (distanceHome > instance.configData.HunterData.HunterRoam) { ReturningToHome = true; return; } if (distanceHome < instance.configData.HunterData.HunterRoam) { Vector3 random = UnityEngine.Random.insideUnitCircle.normalized * instance.configData.HunterData.HunterRoam; Vector3 newPos = instance.GetNavPoint(SpawnPoint + new Vector3(random.x, 0f, random.y)); SettargetDestination(newPos); return; } } if (ReturningToHome && distanceHome > 2) { if (npc.Brain.Navigator.Destination == SpawnPoint) { return; } WipeMemory(); SettargetDestination(SpawnPoint); return; } ReturningToHome = false; } private void SettargetDestination(Vector3 position) { npc.Brain.Navigator.Destination = position; npc.Brain.Navigator.SetDestination(position, BaseNavigator.NavigationSpeed.Slow, 0f, 0f); } void WipeMemory() { if (!npc.HasBrain) { return; } npc.Brain.Senses.Players.Clear(); npc.Brain.Senses.Memory.Players.Clear(); npc.Brain.Senses.Memory.Targets.Clear(); npc.Brain.Senses.Memory.Threats.Clear(); npc.Brain.Senses.Memory.LOS.Clear(); npc.Brain.Senses.Memory.All.Clear(); } void OnDestroy() { if (npc != null && !npc.IsDestroyed) { npc.Kill(); } instance._Hunter.Remove(npc.userID); CancelInvoke("GoHome"); CancelInvoke(nameof(_UseBrain)); } } #endregion #region Configuration void Init() { if (!LoadConfigVariables()) { Puts("Config file issue detected. Please delete file, or check syntax and fix."); return; } Instance = this; // Set the static Instance property HunterName = configData.HunterData.Huntername; Debug = configData.UseDebug; UseBotRespawn = configData.BRS.UseBrs; if (Debug) Puts($"[Debug] Debug is active"); prefix = lang.GetMessage("Prefix", this); harmony = new Harmony("krunghcrow.huntsman"); harmony.PatchAll(); sTitle = configData.HunterData.Huntername; } private ConfigData configData; class ConfigData { [JsonProperty(PropertyName = "Use Debug")] public bool UseDebug = false; [JsonProperty(PropertyName = "BotRespawn Settings")] public BotrespawnSettings BRS = new BotrespawnSettings(); [JsonProperty(PropertyName = "Animals to trigger HuntsMan")] public AnimalSettings AnimalData = new AnimalSettings(); [JsonProperty(PropertyName = "Hunter Settings")] public SettingsHunter HunterData = new SettingsHunter(); } class BotrespawnSettings { [JsonProperty(PropertyName = "Use BotReSpawn")] public bool UseBrs = false; [JsonProperty(PropertyName = "BotReSpawn profile name")] public string ProfileBrs = "huntsman"; } class AnimalSettings { [JsonProperty(PropertyName = "Alpha Animal")] public bool AlphaTrigger = true; [JsonProperty(PropertyName = "Omega Animal")] public bool OmegaTrigger = true; [JsonProperty(PropertyName = "Bear")] public bool BearTrigger = true; [JsonProperty(PropertyName = "Polar Bear")] public bool PBearTrigger = true; [JsonProperty(PropertyName = "Boar")] public bool BoarTrigger = true; [JsonProperty(PropertyName = "Chicken")] public bool ChickenTrigger = true; [JsonProperty(PropertyName = "Horse")] public bool HorseTrigger = true; [JsonProperty(PropertyName = "Stag")] public bool StagTrigger = true; [JsonProperty(PropertyName = "Wolf")] public bool WolfTrigger = true; [JsonProperty(PropertyName = "Wolf2")] public bool WolfTrigger2 = true; } class SettingsHunter { [JsonProperty(PropertyName = "Custom Hunter Name")] public string Huntername = "HuntsMan"; [JsonProperty(PropertyName = "spawn chance (1-100%)")] public float HunterSpawnrate = 10f; [JsonProperty(PropertyName = "Spawn Amount")] public int HunterAmount = 1; [JsonProperty(PropertyName = "spawn radius")] public int Radius = 15; [JsonProperty(PropertyName = "Health")] public int HunterHealth = 250; [JsonProperty(PropertyName = "Max Roam Distance")] public int HunterRoam = 20; [JsonProperty(PropertyName = "Damage multiplier")] public float HunterDamageScale = 0.6f; [JsonProperty(PropertyName = "Lifetime (minutes)")] public float HunterLife = 10f; [JsonProperty(PropertyName = "Use kit (clothing)")] public bool UseKit = false; [JsonProperty(PropertyName = "Kit ID")] public List KitName = new List(); [JsonProperty(PropertyName = "Show messages")] public bool ShowMsg = true; [JsonProperty(PropertyName = "Hunters drop a Backpack with loot")] public bool UseLoot = false; [JsonProperty(PropertyName = "Use Random Skins")] public bool RandomSkins { get; set; } = true; [JsonProperty(PropertyName = "Spawn Min Amount Items")] public int MinAmount { get; set; } = 2; [JsonProperty(PropertyName = "Spawn Max Amount Items")] public int MaxAmount { get; set; } = 6; [JsonProperty(PropertyName = "Loot Table", ObjectCreationHandling = ObjectCreationHandling.Replace)] public List Loot { get; set; } = DefaultLoot; } private bool LoadConfigVariables() { try { configData = Config.ReadObject(); } catch { return false; } SaveConf(); return true; } protected override void LoadDefaultConfig() { Puts("Creating new config file."); configData = new ConfigData(); SaveConf(); } void SaveConf() => Config.WriteObject(configData, true); #endregion #region LanguageAPI protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["Hunter_Spawned"] = "You killed the {0} now pay the price !!!!", ["Hunter_Spawned_Backpack"] = "{0} Dropped his Backpack !", ["Prefix"] = "[HuntsMan] : ", ["info"] = "\nThe [HuntsMan] Gives u the luck (or not) to spawn a Hunter that wants to kill you for killing Wild Game during offseason", }, this); } #endregion #region Commands [ChatCommand("hminfo")] void cmdhmversion(BasePlayer player, string cmd, string[] args) { Player.Message(player, string.Format(msg("Current Version v", player.UserIDString)) + this.Version.ToString() + " By : " + this.Author.ToString() + msginfo("info"), chaticon); } #endregion #region External Hooks object OnNpcKits(BasePlayer player) { if (player?.gameObject?.GetComponent() != null) return true; return null; } void OnBotReSpawnNPCSpawned(ScientistNPC npc, string profile, string group) { if (profile == configData.BRS.ProfileBrs) { npc.displayName = HunterName; _Hunter.Add(npc.userID); npc.SendNetworkUpdate(); AddSuicideTimer(npc); Effect.server.Run("assets/bundled/prefabs/fx/player/howl.prefab", npc.transform.position); if (Debug) Puts($"BotReSpawn spawned {profile} with name ({npc.displayName})"); Interface.CallHook("OnHuntsmanSpawned", npc); } } #endregion #region Oxide Hooks void Unload() { Hunters[] hunters = UnityEngine.Object.FindObjectsOfType(); if (hunters != null) { foreach (Hunters hunter in hunters) UnityEngine.Object.Destroy(hunter); } BotReSpawn?.Call("RemoveGroupSpawn", "huntsman"); harmony.UnpatchAll(harmony.Id); } void OnEntityDeath(BaseAnimalNPC animal , HitInfo info) { if (animal == null || info.InitiatorPlayer == null) return; if (info.InitiatorPlayer.userID.IsSteamId() || info.Initiator is BasePet) ; { if (Interface.CallHook("CanSpawnHuntsman" , animal) != null) { return; } try { AnimalCheck(animal); if (UnityEngine.Random.Range(0f , 1f) <= configData.HunterData.HunterSpawnrate / 100) { if (animal != null && CanTrigger == true) { if (BlockSpawn) { if (Debug) Puts($"{info.InitiatorPlayer.displayName} shot down a ChickenBow Chicken and a HuntsMan was skipped"); return; } for (int i = 0; i < configData.HunterData.HunterAmount; i++) { if (UseBotRespawn) { Vector3 location = animal.transform.position; string _Profile = configData.BRS.ProfileBrs; string[] Spawn = BotReSpawn?.Call("AddGroupSpawn" , location , _Profile , _Profile , 1) as string[]; if (Debug) Puts($"[BotReSpawn] : {Spawn[1]}"); IsSpawned = true; } else if (!UseBotRespawn) Spawnnpc(animal.transform.position); } Puts($"{info.InitiatorPlayer.displayName} shot down a {animal.name} and a HuntsMan tried to arrest him/her"); if (configData.HunterData.ShowMsg && IsSpawned == true) { BasePlayer player = info.InitiatorPlayer; string AnimalName = animal.name; if (AnimalName.Contains("assets/rust.ai/agents/") || AnimalName.Contains(".prefab")) AnimalName = animal.ShortPrefabName; Player.Message(player , string.Format(msg($"Hunter_Spawned" , player.UserIDString) , AnimalName) , chaticon); } return; } return; } } catch { } } return; } void OnEntityDeath(BaseNPC2 animal , HitInfo info) { if (animal == null || info.InitiatorPlayer == null) return; if (info.InitiatorPlayer.userID.IsSteamId() || info.Initiator is BasePet) ; { if (Interface.CallHook("CanSpawnHuntsman" , animal) != null) { return; } try { AnimalCheck(animal); if (UnityEngine.Random.Range(0f , 1f) <= configData.HunterData.HunterSpawnrate / 100) { if (animal != null && CanTrigger == true) { for (int i = 0; i < configData.HunterData.HunterAmount; i++) { if (UseBotRespawn) { Vector3 location = animal.transform.position; string _Profile = configData.BRS.ProfileBrs; string[] Spawn = BotReSpawn?.Call("AddGroupSpawn" , location , _Profile , _Profile , 1) as string[]; if (Debug) Puts($"[BotReSpawn] : {Spawn[1]}"); IsSpawned = true; } else if (!UseBotRespawn) Spawnnpc(animal.transform.position); } Puts($"{info.InitiatorPlayer.displayName} shot down a {animal.name} and a HuntsMan tried to arrest him/her"); if (configData.HunterData.ShowMsg && IsSpawned == true) { BasePlayer player = info.InitiatorPlayer; string AnimalName = animal.name; if (AnimalName.Contains("assets/rust.ai/agents/") || AnimalName.Contains(".prefab")) AnimalName = animal.ShortPrefabName; Player.Message(player , string.Format(msg($"Hunter_Spawned" , player.UserIDString) , AnimalName) , chaticon); } return; } return; } } catch { } } return; } void OnEntityDeath(ScientistNPC hunter, HitInfo info) { if (hunter == null || info?.Initiator == null) return; if (_Hunter.Contains(hunter.userID)) { BasePlayer player = info.InitiatorPlayer; if (player == null || !player.IsValid()) return; if (info.InitiatorPlayer.userID.IsSteamId() || info.Initiator is BasePet) ; { if (configData.HunterData.UseLoot) { Player.Message(player, string.Format(msg("Hunter_Spawned_Backpack", player.UserIDString), hunter.displayName), chaticon); string _displayname = hunter.displayName; SpawnHunterLoot(hunter.transform.position + new Vector3(0f, 0.5f, 0f), hunter.transform.rotation, _displayname); } } Interface.CallHook("OnHuntsmanKilled" , hunter); } } private object OnNpcTarget(BasePlayer attacker, BasePlayer target) { if (attacker != null && _Hunter.Contains(attacker.userID)) { if (target.IsSleeping() || !target.userID.IsSteamId() || !target is BasePet) return true; } if (attacker != null && target != null && _Hunter.Contains(target.userID)) { return true; } return null; } void OnEntitySpawned(NPCPlayerCorpse corpse) { if (corpse == null || corpse.IsDestroyed) return; ulong id = corpse.playerSteamID; if(!(UseBotRespawn) && _Hunter.Contains(corpse.playerSteamID)) { try { NextTick(() => { if (Debug) Puts($"NPC is a {HunterName} so corpse injection is possible"); Item hunterskull = ItemManager.CreateByName("skull.human", 1, 0); ItemAmount HunterSkull = new ItemAmount() { itemDef = hunterskull.info, amount = 1, startAmount = 1 }; Item hunterflesh = ItemManager.CreateByName("humanmeat.raw", 1, 0); ItemAmount HunterFlesh = new ItemAmount() { itemDef = hunterflesh.info, amount = 6, startAmount = 1 }; var dispenser = corpse.GetComponent(); if (dispenser != null) { dispenser.containedItems.Add(HunterSkull); dispenser.containedItems.Add(HunterFlesh); dispenser.Initialize(); if (Debug) Puts($"[Debug] Huntsman Corpse was injected with [Skull & Human flesh]"); } }); return; } catch { return; } } if (Debug && (UseBotRespawn) && _Hunter.Contains(corpse.playerSteamID)) Puts($"[Debug] BotReSpawn will cover the [Skull] injection"); } #endregion #region API private bool IsHuntsmanNpc(global::HumanNPC npc) { if (npc != null) { if (_Hunter.Contains(npc.userID)) return true; return false; } return false; } #endregion #region Event Helpers public Vector3 GetNavPoint(Vector3 position) { NavMeshHit hit; if (!NavMesh.SamplePosition(position, out hit, 5, -1)) { return position; } else if (Physics.RaycastAll(hit.position + new Vector3(0, 100, 0), Vector3.down, 99f, 1235288065).Any()) { return position; } else if (hit.position.y < TerrainMeta.WaterMap.GetHeight(hit.position)) { return position; } position = hit.position; return position; } public static Vector3 CheckPos(Vector3 pos) { NavMeshHit navMeshHit; if (!NavMesh.SamplePosition(pos, out navMeshHit, 2, 1)) pos = Vector3.zero; else if (WaterLevel.GetWaterDepth(pos, true, false) > 0) pos = Vector3.zero; else if (Physics.RaycastAll(navMeshHit.position + new Vector3(0, 100, 0), Vector3.down, 99f, 1235288065).Any()) pos = Vector3.zero; else pos = navMeshHit.position; return pos; } private void AnimalCheck(object animal) { CanTrigger = true; if (animal is BaseAnimalNPC baseAnimal) { // Existing animal type checks if (baseAnimal is Bear && configData.AnimalData.BearTrigger == false) CanTrigger = false; if (baseAnimal is Polarbear && configData.AnimalData.PBearTrigger == false) CanTrigger = false; if (baseAnimal is Boar && configData.AnimalData.BoarTrigger == false) CanTrigger = false; if (baseAnimal is Chicken) { if (configData.AnimalData.ChickenTrigger == false) { CanTrigger = false; } else { if (ChickenBow != null && ChickenBow.Call("IsSpawnedChicken" , baseAnimal.net.ID) == true) { BlockSpawn = true; } else { BlockSpawn = false; } } } if (baseAnimal is Horse && configData.AnimalData.HorseTrigger == false) CanTrigger = false; if (baseAnimal is Stag && configData.AnimalData.StagTrigger == false) CanTrigger = false; if (baseAnimal is Wolf && configData.AnimalData.WolfTrigger == false) CanTrigger = false; } else if (animal is Wolf2) // Specific handling for Wolf2 { // Assuming that Wolf2 should use the same WolfTrigger setting CanTrigger = configData.AnimalData.WolfTrigger2; } else { CanTrigger = false; // If animal isn't recognized, prevent triggering } // Additional checks for Alpha and Omega names if animal supports the name property if (animal is BaseNPC2 npc) { if (npc.name.Contains("Alpha")) { CanTrigger = configData.AnimalData.AlphaTrigger; } else if (npc.name.Contains("Omega")) { CanTrigger = configData.AnimalData.OmegaTrigger; } } if (Debug) Puts($"[Debug] {animal.GetType().Name} kill was triggered and [CanTrigger] is {CanTrigger}"); } private void GiveLoadout(ScientistNPC npc) { if (npc != null) { var inv_belt = npc.inventory.containerBelt; var inv_wear = npc.inventory.containerWear; Item hat = ItemManager.CreateByName("hat.boonie", 1, 841998387); Item hoodie = ItemManager.CreateByName("hoodie", 1, 852449747); Item pants = ItemManager.CreateByName("pants", 1, 1464539276); Item boots = ItemManager.CreateByName("shoes.boots", 1, 839852365); Item weapon1 = ItemManager.CreateByName("shotgun.pump", 1, 0); inv_wear.Clear(); if (hat != null) hat.MoveToContainer(inv_wear); if (hoodie != null) hoodie.MoveToContainer(inv_wear); if (pants != null) pants.MoveToContainer(inv_wear); if (boots != null) boots.MoveToContainer(inv_wear); if (weapon1 != null) weapon1.MoveToContainer(inv_belt); npc.inventory.UpdatedVisibleHolsteredItems(); npc.EquipWeapon(); string scientistSuitShortname = "hazmatsuit_scientist"; Item SuitItem = npc.inventory.containerMain.itemList.FirstOrDefault(item => item.info.shortname == scientistSuitShortname); if (SuitItem != null) { SuitItem.RemoveFromContainer(); } } else { Puts("Huntsman was not fully spawned yet skipping [GiveLoadout]"); } } private void Spawnnpc(Vector3 position) { int radius = configData.HunterData.Radius; Vector3 pos = position + UnityEngine.Random.onUnitSphere * radius; pos.y = TerrainMeta.HeightMap.GetHeight(pos); CheckPos(pos); if (pos == Vector3.zero) return; ScientistNPC npc = (ScientistNPC)GameManager.server.CreateEntity(hunter, pos, new Quaternion(), true); npc.Spawn(); StoredNames[npc.userID] = HunterName; Interface.CallHook("OnHuntsmanSpawn" , npc); _Hunter.Add(npc.userID); timer.Once(0.3f, () => { if (npc == null) return; var mono = npc.gameObject.AddComponent(); mono.SpawnPoint = pos; npc.startHealth = configData.HunterData.HunterHealth; npc.InitializeHealth(configData.HunterData.HunterHealth, configData.HunterData.HunterHealth); npc.damageScale = configData.HunterData.HunterDamageScale; (npc as ScientistNPC).radioChatterType = ScientistNPC.RadioChatterType.NONE; (npc as ScientistNPC).DeathEffects = new GameObjectRef[0]; if (configData.HunterData.UseKit && configData.HunterData.KitName.Count > 0) { object checkKit = Kits?.CallHook("GetKitInfo", configData.HunterData.KitName[new System.Random().Next(configData.HunterData.KitName.Count())]); if (checkKit == null) { GiveLoadout(npc); PrintWarning($"Kit for {HunterName} does not exist - Using a default Hunter outfit."); } else { npc.inventory.Strip(); Kits?.Call($"GiveKit", npc, configData.HunterData.KitName[new System.Random().Next(configData.HunterData.KitName.Count())]); npc.inventory.UpdatedVisibleHolsteredItems(); npc.EquipWeapon(); } } if (!configData.HunterData.UseKit || configData.HunterData.KitName.Count == 0) { GiveLoadout(npc); } npc.EquipWeapon(); }); if (npc.IsHeadUnderwater()) { npc.Kill(); if (Debug) timer.Once(1f, () => { Puts($"{npc} skipped its spawn (spawned under water)"); }); IsSpawned = false; return; } if (!npc.IsOutside()) { npc.Kill(); if (Debug) timer.Once(1f, () => { Puts($"{npc} skipped its spawn (No valid spawnpoint)"); }); IsSpawned = false; return; } IsSpawned = true; Interface.CallHook("OnHuntsmanSpawned", npc); var id = npc.userID; timer.Once(1f, () => { Puts($"{npc} spawned"); }); Effect.server.Run("assets/bundled/prefabs/fx/player/howl.prefab", npc.transform.position); AddSuicideTimer(npc); } private void AddSuicideTimer(ScientistNPC npc) { timer.Once(configData.HunterData.HunterLife * 60, () => { if (npc != null) { _Hunter.Remove(npc.userID); npc.Kill(); if (Debug) Puts($"{npc} Died of natural causes!!!"); return; } if (Debug) Puts($"{npc} was killed before ending of timer"); return; }); } #endregion #region Loot System private static List DefaultLoot { get { return new List { new LootProfile { probability = 1f, shortname = "ammo.pistol", name = "", amountMin = 128, amount = 256, skin = 0 }, new LootProfile { probability = 1f, shortname = "ammo.pistol.fire", name = "", amountMin = 128, amount = 256, skin = 0 }, new LootProfile { probability = 1f, shortname = "ammo.rifle.explosive", name = "", amountMin = 128, amount = 256, skin = 0 }, new LootProfile { probability = 1f, shortname = "ammo.rifle.hv", name = "", amountMin = 128, amount = 256, skin = 0 }, new LootProfile { probability = 1f, shortname = "ammo.rifle.incendiary", name = "", amountMin = 128, amount = 256, skin = 0 }, new LootProfile { probability = 1f, shortname = "ammo.shotgun", name = "", amountMin = 128, amount = 256, skin = 0 }, new LootProfile { probability = 1f, shortname = "ammo.shotgun", name = "", amountMin = 128, amount = 256, skin = 0 }, new LootProfile { probability = 1f, shortname = "explosive.timed", name = "", amountMin = 1, amount = 5, skin = 0 }, new LootProfile { probability = 1f, shortname = "explosives", name = "", amountMin = 200, amount = 200, skin = 0 }, new LootProfile { probability = 1f, shortname = "pistol.m92", name = "", amountMin = 1, amount = 1, skin = 0 }, new LootProfile { probability = 1f, shortname = "shotgun.spas12", name = "", amountMin = 1, amount = 1, skin = 0 }, new LootProfile { probability = 1f, shortname = "pickaxe", name = "", amountMin = 1, amount = 1, skin = 0 }, new LootProfile { probability = 1f, shortname = "hatchet", name = "", amountMin = 1, amount = 1, skin = 0 }, new LootProfile { probability = 1f, shortname = "can.beans", name = "", amountMin = 3, amount = 5, skin = 0 }, new LootProfile { probability = 1f, shortname = "can.tuna", name = "", amountMin = 3, amount = 5, skin = 0 }, new LootProfile { probability = 1f, shortname = "black.raspberries", name = "", amountMin = 3, amount = 5, skin = 0 }, new LootProfile { probability = 0.0f, shortname = "box.repair.bench", name = "Recycler", amountMin = 1, amount = 1, skin = 1594245394 }, new LootProfile { probability = 0.6f, shortname = "hmlmg", name = "", amountMin = 1, amount = 1, skin = 0 }, new LootProfile { probability = 0.6f, shortname = "supply.signal", name = "", amountMin = 1, amount = 1, skin = 0 }, }; } } public class LootProfile { public float probability { get; set; } = 1f; public string shortname { get; set; } public string name { get; set; } = ""; public ulong skin { get; set; } public int amountMin { get; set; } public int amount { get; set; } } private void SpawnHunterLoot(Vector3 pos, Quaternion rot,string _name) { var backpack = GameManager.server.CreateEntity(StringPool.Get(1519640547), pos, rot, true) as DroppedItemContainer; if (backpack == null) return; backpack.inventory = new ItemContainer(); backpack.inventory.ServerInitialize(null , 36); backpack.inventory.GiveUID(); backpack.inventory.entityOwner = backpack; backpack.inventory.SetFlag(ItemContainer.Flag.NoItemInput , true); backpack.Spawn(); SpawnLoot(backpack.inventory, configData.HunterData.Loot.ToList()); } private void SpawnLoot(ItemContainer container, List loot) { int total = UnityEngine.Random.Range(Math.Min(loot.Count, configData.HunterData.MinAmount), Math.Min(loot.Count, configData.HunterData.MaxAmount)); if (total == 0 || loot.Count == 0) { return; } container.capacity = total; ItemDefinition def; List skins; LootProfile lootItem; for (int j = 0; j < total; j++) { if (loot.Count == 0) { break; } lootItem = loot.GetRandom(); loot.Remove(lootItem); if (lootItem.amount <= 0) { continue; } string shortname = lootItem.shortname; if (UnityEngine.Random.value > lootItem.probability) { if (!string.IsNullOrEmpty(lootItem.name)) Puts($"Skipping Custom({j + 1}) : {lootItem.name} : probability = {lootItem.probability}"); else if (string.IsNullOrEmpty(lootItem.name)) Puts($"Skipping Normal({j + 1}) : {lootItem.shortname} : probability = {lootItem.probability}"); j--; continue; } bool isBlueprint = shortname.EndsWith(".bp"); if (isBlueprint) { shortname = shortname.Replace(".bp", string.Empty); } def = ItemManager.FindItemDefinition(shortname); if (def == null) { Puts("Invalid shortname: {0}", lootItem.shortname); continue; } ulong skin = lootItem.skin; if (configData.HunterData.RandomSkins && skin == 0) { skins = GetItemSkins(def); if (skins.Count > 0) { skin = skins.GetRandom(); } } int amount = lootItem.amount; if (amount <= 0) { continue; } if (lootItem.amountMin > 0 && lootItem.amountMin < lootItem.amount) { amount = UnityEngine.Random.Range(lootItem.amountMin, lootItem.amount); } Item item; if (isBlueprint) { item = ItemManager.CreateByItemID(-996920608, 1, 0); if (item == null) continue; item.blueprintTarget = def.itemid; item.amount = amount; } else item = ItemManager.Create(def, amount, skin); if (!item.MoveToContainer(container, -1, false)) { item.Remove(); } if (!string.IsNullOrEmpty(lootItem.name)) { item.name = lootItem.name; } if (Debug) { if (string.IsNullOrEmpty(item.name)) { Puts($"Normal({j + 1}) : {item.info.shortname} : Amount {item.amount} SkinID : {item.skin}"); } if (!string.IsNullOrEmpty(item.name)) { Puts($"Custom({j + 1}) : {item.name} : Amount : {item.amount} SkinID : {item.skin}"); } } } } private List GetItemSkins(ItemDefinition def) { List skins; if (!Skins.TryGetValue(def.shortname, out skins)) { Skins[def.shortname] = skins = ExtractItemSkins(def, skins); } return skins; } private List ExtractItemSkins(ItemDefinition def, List skins) { skins = new List(); foreach (var skin in def.skins) { skins.Add(Convert.ToUInt64(skin.id)); } foreach (var asi in Rust.Workshop.Approved.All.Values) { if (!string.IsNullOrEmpty(asi.Skinnable.ItemName) && asi.Skinnable.ItemName == def.shortname) { skins.Add(Convert.ToUInt64(asi.WorkshopdId)); } } return skins; } #endregion #region Message helper private string msg(string key, string id = null) => prefix + lang.GetMessage(key, this, id); private string msginfo(string key, string id = null) => lang.GetMessage(key, this, id); #endregion } }