C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.AI;
using Facepunch;
using Oxide.Plugins.BetterNpcExtensionMethods;
namespace Oxide.Plugins {
[Info("BetterNpc", "KpucTaJl", "1.3.1")]
internal class BetterNpc : RustPlugin {
#region Config
private const bool En = false;
private PluginConfig _config;
protected override void LoadDefaultConfig() {
Puts("Creating a default config...");
_config = PluginConfig.DefaultConfig();
_config.PluginVersion = Version;
SaveConfig();
Puts("Creation of the default config completed!");
}
protected override void LoadConfig() {
base.LoadConfig();
_config = Config.ReadObject<PluginConfig>();
if (_config.PluginVersion < Version) UpdateConfigValues();
}
private void UpdateConfigValues() {
Puts("Config update detected! Updating config values...");
if (_config.PluginVersion < new VersionNumber(1, 2, 4)) {
_config.CanSleep = true;
_config.SleepDistance = 100f;
}
_config.PluginVersion = Version;
Puts("Config update completed!");
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config);
public class ItemConfig {
[JsonProperty("ShortName")]
public string ShortName {
get;
set;
}
[JsonProperty(En ? "Minimum" : "Минимальное кол-во")]
public int MinAmount {
get;
set;
}
[JsonProperty(En ? "Maximum" : "Максимальное кол-во")]
public int MaxAmount {
get;
set;
}
[JsonProperty(En ? "Chance [0.0-100.0]" : "Шанс выпадения предмета [0.0-100.0]")]
public float Chance {
get;
set;
}
[JsonProperty(En ? "Is this a blueprint? [true/false]" : "Это чертеж? [true/false]")]
public bool IsBluePrint {
get;
set;
}
[JsonProperty("SkinID (0 - default)")]
public ulong SkinID {
get;
set;
}
[JsonProperty(En ? "Name (empty - default)" : "Название (empty - default)")]
public string Name {
get;
set;
}
}
public class LootTableConfig {
[JsonProperty(En ? "Minimum numbers of items" : "Минимальное кол-во элементов")]
public int Min {
get;
set;
}
[JsonProperty(En ? "Maximum numbers of items" : "Максимальное кол-во элементов")]
public int Max {
get;
set;
}
[JsonProperty(En ? "Use minimum and maximum values? [true/false]" : "Использовать минимальное и максимальное значение? [true/false]")]
public bool UseCount {
get;
set;
}
[JsonProperty(En ? "List of items" : "Список предметов")]
public List<ItemConfig> Items {
get;
set;
}
}
public class PrefabConfig {
[JsonProperty(En ? "Chance [0.0-100.0]" : "Шанс выпадения [0.0-100.0]")]
public float Chance {
get;
set;
}
[JsonProperty(En ? "The path to the prefab" : "Путь к prefab-у")]
public string PrefabDefinition {
get;
set;
}
}
public class PrefabLootTableConfig {
[JsonProperty(En ? "Minimum numbers of prefabs" : "Минимальное кол-во prefab-ов")]
public int Min {
get;
set;
}
[JsonProperty(En ? "Maximum numbers of prefabs" : "Максимальное кол-во prefab-ов")]
public int Max {
get;
set;
}
[JsonProperty(En ? "Use minimum and maximum values? [true/false]" : "Использовать минимальное и максимальное значение? [true/false]")]
public bool UseCount {
get;
set;
}
[JsonProperty(En ? "List of prefabs" : "Список prefab-ов")]
public List<PrefabConfig> Prefabs {
get;
set;
}
}
public class NpcBelt {
[JsonProperty("ShortName")]
public string ShortName {
get;
set;
}
[JsonProperty(En ? "Amount" : "Кол-во")]
public int Amount {
get;
set;
}
[JsonProperty("SkinID (0 - default)")]
public ulong SkinID {
get;
set;
}
[JsonProperty(En ? "Mods" : "Модификации на оружие")]
public List<string> Mods {
get;
set;
}
[JsonProperty(En ? "Ammo" : "Боеприпасы")]
public string Ammo {
get;
set;
}
}
public class NpcWear {
[JsonProperty("ShortName")]
public string ShortName {
get;
set;
}
[JsonProperty("SkinID (0 - default)")]
public ulong SkinID {
get;
set;
}
}
public class NpcEconomic {
[JsonProperty("Economics")]
public double Economics {
get;
set;
}
[JsonProperty(En ? "Server Rewards (minimum 1)" : "Server Rewards (минимум 1)")]
public int ServerRewards {
get;
set;
}
[JsonProperty(En ? "IQEconomic (minimum 1)" : "IQEconomic (минимум 1)")]
public int IQEconomic {
get;
set;
}
}
public class NpcConfig {
[JsonProperty(En ? "Names" : "Названия")]
public List<string> Names {
get;
set;
}
[JsonProperty(En ? "Health" : "Кол-во ХП")]
public float Health {
get;
set;
}
[JsonProperty(En ? "Roam Range" : "Дальность патрулирования местности")]
public float RoamRange {
get;
set;
}
[JsonProperty(En ? "Chase Range" : "Дальность погони за целью")]
public float ChaseRange {
get;
set;
}
[JsonProperty(En ? "Attack Range Multiplier" : "Множитель радиуса атаки")]
public float AttackRangeMultiplier {
get;
set;
}
[JsonProperty(En ? "Sense Range" : "Радиус обнаружения цели")]
public float SenseRange {
get;
set;
}
[JsonProperty(En ? "Target Memory Duration [sec.]" : "Длительность памяти цели [sec.]")]
public float MemoryDuration {
get;
set;
}
[JsonProperty(En ? "Scale damage" : "Множитель урона")]
public float DamageScale {
get;
set;
}
[JsonProperty(En ? "Aim Cone Scale" : "Множитель разброса")]
public float AimConeScale {
get;
set;
}
[JsonProperty(En ? "Detect the target only in the NPC's viewing vision cone? [true/false]" : "Обнаруживать цель только в углу обзора NPC? [true/false]")]
public bool CheckVisionCone {
get;
set;
}
[JsonProperty(En ? "Vision Cone" : "Угол обзора")]
public float VisionCone {
get;
set;
}
[JsonProperty(En ? "Speed" : "Скорость")]
public float Speed {
get;
set;
}
[JsonProperty(En ? "Minimum time of appearance after death (not used for Events) [sec.]" : "Минимальное время появления после смерти (не используется для Events) [sec.]")]
public float MinTime {
get;
set;
}
[JsonProperty(En ? "Maximum time of appearance after death (not used for Events) [sec.]" : "Максимальное время появления после смерти (не используется для Events) [sec.]")]
public float MaxTime {
get;
set;
}
[JsonProperty(En ? "Disable radio effects? [true/false]" : "Отключать эффекты рации? [true/false]")]
public bool DisableRadio {
get;
set;
}
[JsonProperty(En ? "Is this a stationary NPC? [true/false]" : "Это стационарный NPC? [true/false]")]
public bool Stationary {
get;
set;
}
[JsonProperty(En ? "Remove a corpse after death? (it is recommended to use the true value to improve performance) [true/false]" : "Удалять труп после смерти? (рекомендуется использовать значение true для повышения производительности) [true/false]")]
public bool IsRemoveCorpse {
get;
set;
}
[JsonProperty(En ? "Wear items" : "Одежда")]
public List<NpcWear> WearItems {
get;
set;
}
[JsonProperty(En ? "Belt items" : "Быстрые слоты")]
public List<NpcBelt> BeltItems {
get;
set;
}
[JsonProperty(En ? "Kits (it is recommended to use the previous 2 settings to improve performance)" : "Kits (рекомендуется использовать предыдущие 2 пункта настройки для повышения производительности)")]
public List<string> Kits {
get;
set;
}
}
public class PresetConfig {
[JsonProperty(En ? "Enabled? [true/false]" : "Включен? [true/false]")]
public bool Enabled {
get;
set;
}
[JsonProperty(En ? "Minimum numbers - Day" : "Минимальное кол-во днем")]
public int MinDay {
get;
set;
}
[JsonProperty(En ? "Maximum numbers - Day" : "Максимальное кол-во днем")]
public int MaxDay {
get;
set;
}
[JsonProperty(En ? "Minimum numbers - Night" : "Минимальное кол-во ночью")]
public int MinNight {
get;
set;
}
[JsonProperty(En ? "Maximum numbers - Night" : "Максимальное кол-во ночью")]
public int MaxNight {
get;
set;
}
[JsonProperty(En ? "NPCs setting" : "Настройки NPC")]
public NpcConfig Config {
get;
set;
}
[JsonProperty(En ? "The amount of economics that is given for killing the NPC" : "Кол-во экономики, которое выдается за убийство NPC")]
public NpcEconomic Economic {
get;
set;
}
[JsonProperty(En ? "Type of appearance (0 - random; 1 - own list) (not used for Road and Biome)" : "Тип появления (0 - рандомное; 1 - собственный список) (не используется для Road и Biome)")]
public int TypeSpawn {
get;
set;
}
[JsonProperty(En ? "Own list of locations (not used for Road and Biome)" : "Собственный список расположений (не используется для Road и Biome)")]
public List<string> OwnPositions {
get;
set;
}
[JsonProperty(En ? "If the NPC ends up below ocean sea level, should the NPC return to it's place of appearance? [true/false]" : "Должен ли Npc убегать на место своего появления, если он находится ниже уровня океана? [true/false]")]
public bool CanRunAwayWater {
get;
set;
}
[JsonProperty(En ? "Type of navigation grid (0 - used mainly on the island, 1 - used mainly under water or under land, as well as outside the map, can be used on some monuments)" : "Тип навигационной сетки (0 - используется в основном на острове, 1 - используется в основном под водой или землей, а также за пределами карты, может использоваться на некоторых монументах)")]
public int TypeNavMesh {
get;
set;
}
[JsonProperty(En ? "The path to the crate that appears at the place of death (empty - not used)" : "Путь к ящику, который появляется на месте смерти (empty - not used)")]
public string CratePrefab {
get;
set;
}
[JsonProperty(En ? "Which loot table should the plugin use? (0 - default; 1 - own; 2 - AlphaLoot; 3 - CustomLoot; 4 - loot table of the Rust objects; 5 - combine the 1 and 4 methods)" : "Какую таблицу предметов необходимо использовать? (0 - стандартную; 1 - собственную; 2 - AlphaLoot; 3 - CustomLoot; 4 - таблица предметов объектов Rust; 5 - совместить 1 и 4 методы)")]
public int TypeLootTable {
get;
set;
}
[JsonProperty(En ? "Loot table from prefabs (if the loot table type is 4 or 5)" : "Таблица предметов из prefab-ов (если тип таблицы предметов - 4 или 5)")]
public PrefabLootTableConfig PrefabLootTable {
get;
set;
}
[JsonProperty(En ? "Own loot table (if the loot table type is 1 or 5)" : "Собственная таблица предметов (если тип таблицы предметов - 1 или 5)")]
public LootTableConfig OwnLootTable {
get;
set;
}
}
private class PluginConfig {
[JsonProperty(En ? "Start time of the day" : "Время начала дня")]
public string StartDayTime {
get;
set;
}
[JsonProperty(En ? "Start time of the night" : "Время начала ночи")]
public string StartNightTime {
get;
set;
}
[JsonProperty(En ? "Use the PVE mode of the plugin? (only for users PveMode plugin)" : "Использовать PVE режим работы плагина? (только для тех, кто использует плагин PveMode)")]
public bool Pve {
get;
set;
}
[JsonProperty(En ? "The distance from the center of the safe zone to the nearest place where the NPC appears [m.]" : "Расстояние от центра безопасной зоны до ближайшего места появления NPC [m.]")]
public float SafeZoneRange {
get;
set;
}
[JsonProperty(En ? "List of Npc types that should not be deleted" : "Список типов Npc, которые не должны удаляться")]
public HashSet<string> OtherNpc {
get;
set;
}
[JsonProperty(En ? "Run debug.puzzlereset command when the plugin is loaded or reloaded to refresh IO, puzzle, NPCs at Facepunch Monuments [true/false]" : "Обновлять головоломки на монументах во время загрузки плагина? (debug.puzzlereset) [true/false]")]
public bool PuzzleReset {
get;
set;
}
[JsonProperty(En ? "Enable a simplified log of loading progress in the server console? (Simplified log intended for advanced users who want to reduce loading messages) [true/false]" : "Включить упрощенный вариант сообщений в консоли во время загрузки плагина? [true/false]")]
public bool EnabledMinLogs {
get;
set;
}
[JsonProperty(En ? "Enable sleeping NPCs outside of player range to improve performance? [true/false]" : "Использовать режим сна для NPС, когда игрока нет рядом с NPC? (используется для повышения производительности) [true/false]")]
public bool CanSleep {
get;
set;
}
[JsonProperty(En ? "The range from NPC to player at which to wake sleeping NPCs [m.]" : "Расстояние от игрока до NPC, чтобы отключать спящий режим у NPC [m.]")]
public float SleepDistance {
get;
set;
}
[JsonProperty(En ? "Configuration version" : "Версия конфигурации")]
public VersionNumber PluginVersion {
get;
set;
}
public static PluginConfig DefaultConfig() {
return new PluginConfig() {
StartDayTime = "8:00", StartNightTime = "20:00", Pve = false, SafeZoneRange = 150f, OtherNpc = new HashSet<string> {
"11162132011012", "NpcRaider", "RandomRaider", "56485621526987"
}
, PuzzleReset = true, EnabledMinLogs = false, CanSleep = true, SleepDistance = 100f, PluginVersion = new VersionNumber()
}
;
}
}
#endregion Config
#region Oxide Hooks
[PluginReference]
private readonly Plugin NpcSpawn, PveMode;
private static BetterNpc _ins;
private void Init() => _ins = this;
private void OnServerInitialized() {
if (!plugins.Exists("NpcSpawn")) {
PrintError("NpcSpawn plugin doesn`t exist! (https://drive.google.com/drive/folders/1-18L-mG7yiGxR-PQYvd11VvXC2RQ4ZCu?usp=sharing)");
NextTick(() => Interface.Oxide.UnloadPlugin(Name));
return;
}
_isDay = TOD_Sky.Instance.Cycle.Hour < TimeSpan.Parse(_config.StartNightTime).TotalHours && TOD_Sky.Instance.Cycle.Hour > TimeSpan.Parse(_config.StartDayTime).TotalHours;
_loadCoroutine = ServerMgr.Instance.StartCoroutine(LoadAllFiles());
}
private void Unload() {
if (_loadCoroutine != null) ServerMgr.Instance.StopCoroutine(_loadCoroutine);
if (_checkDayCoroutine != null) ServerMgr.Instance.StopCoroutine(_checkDayCoroutine);
foreach (ControllerSpawnPoint controller in _controllers) if (controller != null) UnityEngine.Object.Destroy(controller.gameObject);
foreach (KeyValuePair<ulong, Dictionary<ScientistNPC, string>> dic in _cargoShipControllers) foreach (KeyValuePair<ScientistNPC, string> npc in dic.Value) if (npc.Key.IsExists()) npc.Key.Kill();
_ins = null;
}
private void OnCorpsePopulate(ScientistNPC npc, NPCPlayerCorpse corpse) {
if (npc == null || corpse == null || npc.skinID != 11162132011012) return;
ControllerSpawnPoint controller = _controllers.FirstOrDefault(x => x.ActiveNpc.Any(y => y.Npc == npc));
if (controller != null) controller.DieNpc(npc, corpse);
if (_cargoShipControllers.Count > 0) {
KeyValuePair<ulong, Dictionary<ScientistNPC, string>> controllerCargoShip = _cargoShipControllers.FirstOrDefault(x => x.Value.ContainsKey(npc));
if (!controllerCargoShip.Equals(default(KeyValuePair<ulong, Dictionary<ScientistNPC, string>>))) {
string shortPrefabName = controllerCargoShip.Value[npc];
NpcConfigCargo config = shortPrefabName == "scientistnpc_cargo_turret_lr300" ? _cargoShipSpawnPoint.NpcStationaryOutsideCargo : shortPrefabName == "scientistnpc_cargo_turret_any" ? _cargoShipSpawnPoint.NpcStationaryInsideCargo : _cargoShipSpawnPoint.NpcMovingCargo;
if (controllerCargoShip.Value.Count == 1) _cargoShipControllers.Remove(controllerCargoShip.Key);
else controllerCargoShip.Value.Remove(npc);
if (!string.IsNullOrEmpty(config.CratePrefab)) {
BaseEntity entity = GameManager.server.CreateEntity(config.CratePrefab, npc.transform.position, npc.transform.rotation);
if (entity == null) PrintWarning($"Unknown entity! ({config.CratePrefab})");
else {
entity.enableSaving = false;
entity.Spawn();
}
}
BasePlayer attacker = npc.lastAttacker as BasePlayer;
if (attacker.IsPlayer()) SendBalance(attacker.userID, config.Economic);
NextTick(() => {
if (config.TypeLootTable == 1 || config.TypeLootTable == 4 || config.TypeLootTable == 5) {
ItemContainer container = corpse.containers[0];
for (int i = container.itemList.Count - 1; i >= 0; i--) {
Item item = container.itemList[i];
item.RemoveFromContainer();
item.Remove();
}
if (config.TypeLootTable == 4 || config.TypeLootTable == 5) AddToContainerPrefab(container, config.PrefabLootTable);
if (config.TypeLootTable == 1 || config.TypeLootTable == 5) AddToContainerItem(container, config.OwnLootTable);
}
if (config.IsRemoveCorpse && corpse.IsExists()) corpse.Kill();
}
);
}
}
}
#endregion Oxide Hooks
#region Day or Night
private Coroutine _checkDayCoroutine = null;
private bool _isDay;
private IEnumerator CheckDay() {
while (true) {
if (_isDay) {
if (TOD_Sky.Instance.Cycle.Hour > TimeSpan.Parse(_config.StartNightTime).TotalHours || TOD_Sky.Instance.Cycle.Hour < TimeSpan.Parse(_config.StartDayTime).TotalHours) {
_isDay = false;
foreach (ControllerSpawnPoint controller in _controllers) controller.SetDay(_isDay);
}
}
else {
if (TOD_Sky.Instance.Cycle.Hour < TimeSpan.Parse(_config.StartNightTime).TotalHours && TOD_Sky.Instance.Cycle.Hour > TimeSpan.Parse(_config.StartDayTime).TotalHours) {
_isDay = true;
foreach (ControllerSpawnPoint controller in _controllers) controller.SetDay(_isDay);
}
}
yield return CoroutineEx.waitForSeconds(30f);
}
}
#endregion Day or Night
#region Controller
private readonly HashSet<ControllerSpawnPoint> _controllers = new HashSet<ControllerSpawnPoint>();
internal class ControllerSpawnPoint : FacepunchBehaviour {
internal string Name;
internal bool IsEvent;
internal bool RemoveOtherNpc;
internal Vector3 Size;
internal List<PresetConfig> Presets;
internal bool IsDay;
internal HashSet<ActiveScientistNpc> ActiveNpc = new HashSet<ActiveScientistNpc>();
private readonly HashSet<DeadScientistNpc> DeadNpc = new HashSet<DeadScientistNpc>();
private int GetAmountPreset(PresetConfig preset) => ActiveNpc.Where(x => x.Preset == preset).Count + DeadNpc.Where(x => x.Preset == preset).Count;
private int GetAmountPresetConfig(PresetConfig preset) => IsDay ? UnityEngine.Random.Range(preset.MinDay, preset.MaxDay + 1) : UnityEngine.Random.Range(preset.MinNight, preset.MaxNight + 1);
private void OnDestroy() {
CancelInvoke(ChangeDeadNpcTime);
foreach (ActiveScientistNpc activeScientistNpc in ActiveNpc) if (activeScientistNpc.Npc.IsExists()) activeScientistNpc.Npc.Kill();
}
internal void Init() {
if (RemoveOtherNpc) {
List<NPCPlayer> list = Pool.Get<List<NPCPlayer>>();
float radius = Size.x > Size.y ? (Size.x > Size.z ? Size.x : Size.z) : (Size.y > Size.z ? Size.y : Size.z);
Vis.Entities(transform.position, radius, list, 1 << 17);
foreach (NPCPlayer npc in list) if (IsOtherNpc(npc)) npc.Kill();
Pool.FreeUnmanaged(ref list);
}
foreach (PresetConfig preset in Presets) {
if (!preset.Enabled) continue;
int amount = GetAmountPresetConfig(preset);
for (int i = 0; i < amount; i++) SpawnNpc(preset);
}
}
private void ChangeDeadNpcTime() {
foreach (DeadScientistNpc deadScientistNpc in DeadNpc) deadScientistNpc.TimeToSpawn--;
while (DeadNpc.Any(x => x.TimeToSpawn == 0)) {
DeadScientistNpc deadScientistNpc = DeadNpc.FirstOrDefault(x => x.TimeToSpawn == 0);
int amountPresetConfig = GetAmountPresetConfig(deadScientistNpc.Preset);
int amountPreset = GetAmountPreset(deadScientistNpc.Preset) - 1;
if (amountPresetConfig > amountPreset) SpawnNpc(deadScientistNpc.Preset);
DeadNpc.Remove(deadScientistNpc);
}
if (DeadNpc.Count == 0) CancelInvoke(ChangeDeadNpcTime);
}
internal void SetDay(bool day) {
IsDay = day;
foreach (PresetConfig preset in Presets) {
int amountPresetConfig = GetAmountPresetConfig(preset);
int amountPreset = GetAmountPreset(preset);
if (amountPresetConfig > amountPreset) {
int amount = amountPresetConfig - amountPreset;
for (int i = 0; i < amount; i++) SpawnNpc(preset);
}
else if (amountPresetConfig < amountPreset) {
int amount = amountPreset - amountPresetConfig;
for (int i = 0; i < amount; i++) KillNpc(preset);
}
}
}
private void SpawnNpc(PresetConfig preset) {
Vector3 pos = Vector3.zero;
int attempts = 0;
while (pos == Vector3.zero && attempts < 100) {
attempts++;
if (Name == "Arid" || Name == "Temperate" || Name == "Tundra" || Name == "Arctic") {
object point = _ins.NpcSpawn.Call("GetSpawnPoint", Name);
if (point is Vector3) pos = (Vector3)point;
}
else if (preset.TypeSpawn == 0) pos = GetRandomSpawnPos();
else pos = transform.TransformPoint(preset.OwnPositions.GetRandom().ToVector3());
if (ActiveNpc.Any(x => x.Npc.IsExists() && Vector3.Distance(x.Npc.transform.position, pos) < 5f)) pos = Vector3.zero;
if (pos.y < -0.25f && preset.CanRunAwayWater) pos = Vector3.zero;
if (pos != Vector3.zero && TriggerSafeZone.allSafeZones.Count > 0) {
TriggerSafeZone nearSafeZone = TriggerSafeZone.allSafeZones.Min(x => Vector3.Distance(pos, x.transform.position));
if (nearSafeZone != null && Vector3.Distance(pos, nearSafeZone.transform.position) < _ins._config.SafeZoneRange) pos = Vector3.zero;
}
}
if (pos != Vector3.zero) {
ScientistNPC npc = (ScientistNPC)_ins.NpcSpawn.Call("SpawnNpc", pos, GetObjectConfig(preset.Config, preset.CanRunAwayWater, preset.TypeNavMesh));
if (npc != null) {
ActiveNpc.Add(new ActiveScientistNpc {
Preset = preset, Npc = npc
}
);
if (_ins._config.Pve && _ins.plugins.Exists("PveMode")) _ins.PveMode.Call("ScientistAddPveMode", npc);
}
}
}
private void KillNpc(PresetConfig preset) {
if (DeadNpc.Any(x => x.Preset == preset)) {
DeadScientistNpc deadScientistNpc = DeadNpc.Where(x => x.Preset == preset).ToList().GetRandom();
DeadNpc.Remove(deadScientistNpc);
}
else {
ActiveScientistNpc activeScientistNpc = ActiveNpc.Where(x => x.Preset == preset).ToList().GetRandom();
if (activeScientistNpc.Npc.IsExists()) activeScientistNpc.Npc.Kill();
ActiveNpc.Remove(activeScientistNpc);
}
}
internal void DieNpc(ScientistNPC npc, NPCPlayerCorpse corpse) {
ActiveScientistNpc activeScientistNpc = ActiveNpc.FirstOrDefault(x => x.Npc == npc);
PresetConfig preset = activeScientistNpc.Preset;
if (!string.IsNullOrEmpty(preset.CratePrefab)) {
BaseEntity entity = GameManager.server.CreateEntity(preset.CratePrefab, npc.transform.position, npc.transform.rotation);
if (entity == null) _ins.PrintWarning($"Unknown entity! ({preset.CratePrefab})");
else {
entity.enableSaving = false;
entity.Spawn();
}
}
if (IsEvent) DeadNpc.Add(new DeadScientistNpc {
Preset = preset, TimeToSpawn = 0
}
);
else {
DeadNpc.Add(new DeadScientistNpc {
Preset = preset, TimeToSpawn = (int)UnityEngine.Random.Range(preset.Config.MinTime, preset.Config.MaxTime)
}
);
if (DeadNpc.Count == 1) InvokeRepeating(ChangeDeadNpcTime, 1f, 1f);
}
ActiveNpc.Remove(activeScientistNpc);
BasePlayer attacker = npc.lastAttacker as BasePlayer;
if (attacker.IsPlayer()) _ins.SendBalance(attacker.userID, preset.Economic);
_ins.NextTick(() => {
if (preset.TypeLootTable == 1 || preset.TypeLootTable == 4 || preset.TypeLootTable == 5) {
ItemContainer container = corpse.containers[0];
for (int i = container.itemList.Count - 1; i >= 0; i--) {
Item item = container.itemList[i];
item.RemoveFromContainer();
item.Remove();
}
if (preset.TypeLootTable == 4 || preset.TypeLootTable == 5) _ins.AddToContainerPrefab(container, preset.PrefabLootTable);
if (preset.TypeLootTable == 1 || preset.TypeLootTable == 5) _ins.AddToContainerItem(container, preset.OwnLootTable);
}
if (preset.Config.IsRemoveCorpse && corpse.IsExists()) corpse.Kill();
if (IsEvent && ActiveNpc.Count == 0) {
_ins._controllers.Remove(this);
Destroy(gameObject);
}
}
);
}
internal bool IsOtherNpc(NPCPlayer npc) {
if (!npc.IsExists()) return false;
if (_ins._config.OtherNpc.Contains(npc.GetType().Name)) return false;
if (_ins._config.OtherNpc.Contains(npc.displayName)) return false;
if (_ins._config.OtherNpc.Contains(npc.skinID.ToString())) return false;
if (!IsInsideNpc(npc)) return false;
if (npc.ShortPrefabName.Contains("scientistnpc_patrol") || npc.ShortPrefabName.Contains("scientistnpc_excavator") || npc.ShortPrefabName.Contains("scientistnpc_roamtethered") || npc.ShortPrefabName.Contains("scientistnpc_full") || npc.ShortPrefabName.Contains("scientistnpc_roam") || npc.ShortPrefabName.Contains("scientistnpc_roam_nvg_variant") || npc.ShortPrefabName.Contains("scientistnpc_oilrig") || npc.ShortPrefabName.Contains("npc_underwaterdweller") || npc.ShortPrefabName.Contains("npc_tunneldweller") || npc.ShortPrefabName.Contains("scarecrow")) return true;
return false;
}
private bool IsInsideNpc(NPCPlayer npc) {
Vector3 localPos = transform.InverseTransformPoint(npc.transform.position);
if (localPos.x < -Size.x || localPos.x > Size.x) return false;
if (localPos.y < -Size.y || localPos.y > Size.y) return false;
if (localPos.z < -Size.z || localPos.z > Size.z) return false;
return true;
}
private Vector3 GetRandomSpawnPos() {
RaycastHit raycastHit;
NavMeshHit navmeshHit;
int attempts = 0;
while (attempts < 10) {
attempts++;
Vector3 pos = transform.TransformPoint(new Vector3(UnityEngine.Random.Range(-Size.x, Size.x), 500f, UnityEngine.Random.Range(-Size.z, Size.z)));
if (!Physics.Raycast(pos, Vector3.down, out raycastHit, 500f, 1 << 16 | 1 << 23)) continue;
pos.y = raycastHit.point.y;
if (!NavMesh.SamplePosition(pos, out navmeshHit, 2f, 1)) continue;
pos = navmeshHit.position;
if (pos.y < -0.25f) continue;
return pos;
}
return Vector3.zero;
}
private static JObject GetObjectConfig(NpcConfig config, bool canRunAwayWater, int typeNavMesh) {
HashSet<string> states = config.Stationary ? new HashSet<string> {
"IdleState", "CombatStationaryState"
}
: new HashSet<string> {
"RoamState", "ChaseState", "CombatState"
}
;
if (config.BeltItems.Any(x => x.ShortName == "rocket.launcher" || x.ShortName == "explosive.timed")) states.Add("RaidState");
return new JObject {
["Name"]
= config.Names.GetRandom(), ["WearItems"] = new JArray {
config.WearItems.Select(x => new JObject {
["ShortName"]
= x.ShortName, ["SkinID"] = x.SkinID
}
)
}
, ["BeltItems"] = new JArray {
config.BeltItems.Select(x => new JObject {
["ShortName"]
= x.ShortName, ["Amount"] = x.Amount, ["SkinID"] = x.SkinID, ["Mods"] = new JArray {
x.Mods
}
, ["Ammo"] = x.Ammo
}
)
}
, ["Kit"] = config.Kits.GetRandom(), ["Health"] = config.Health, ["RoamRange"] = config.RoamRange, ["ChaseRange"] = config.ChaseRange, ["SenseRange"] = config.SenseRange, ["ListenRange"] = config.SenseRange / 2f, ["AttackRangeMultiplier"] = config.AttackRangeMultiplier, ["CheckVisionCone"] = config.CheckVisionCone, ["VisionCone"] = config.VisionCone, ["HostileTargetsOnly"] = false, ["DamageScale"] = config.DamageScale, ["TurretDamageScale"] = 1f, ["AimConeScale"] = config.AimConeScale, ["DisableRadio"] = config.DisableRadio, ["CanRunAwayWater"] = canRunAwayWater, ["CanSleep"] = _ins._config.CanSleep, ["SleepDistance"] = _ins._config.SleepDistance, ["Speed"] = config.Speed, ["AreaMask"] = typeNavMesh == 0 ? 1 : 25, ["AgentTypeID"] = typeNavMesh == 0 ? -1372625422 : 0, ["HomePosition"] = string.Empty, ["MemoryDuration"] = config.MemoryDuration, ["States"] = new JArray {
states
}
}
;
}
internal class ActiveScientistNpc {
public PresetConfig Preset;
public ScientistNPC Npc;
}
internal class DeadScientistNpc {
public PresetConfig Preset;
public int TimeToSpawn;
}
}
#endregion Controller
#region Economy
[PluginReference]
private readonly Plugin Economics, ServerRewards, IQEconomic;
internal void SendBalance(ulong playerId, NpcEconomic economic) {
if (plugins.Exists("Economics") && economic.Economics > 0) Economics.Call("Deposit", playerId.ToString(), economic.Economics);
if (plugins.Exists("ServerRewards") && economic.ServerRewards > 0) ServerRewards.Call("AddPoints", playerId, economic.ServerRewards);
if (plugins.Exists("IQEconomic") && economic.IQEconomic > 0) IQEconomic.Call("API_SET_BALANCE", playerId, economic.IQEconomic);
}
#endregion Economy
#region Loot Spawn
private void AddToContainerPrefab(ItemContainer container, PrefabLootTableConfig lootTable) {
HashSet<string> prefabsInContainer = new HashSet<string>();
if (lootTable.UseCount) {
int count = 0, max = UnityEngine.Random.Range(lootTable.Min, lootTable.Max + 1);
while (count < max) {
foreach (PrefabConfig prefab in lootTable.Prefabs) {
if (prefabsInContainer.Count < lootTable.Prefabs.Count && prefabsInContainer.Contains(prefab.PrefabDefinition)) continue;
if (UnityEngine.Random.Range(0f, 100f) > prefab.Chance) continue;
SpawnIntoContainer(container, prefab.PrefabDefinition);
if (!prefabsInContainer.Contains(prefab.PrefabDefinition)) prefabsInContainer.Add(prefab.PrefabDefinition);
count++;
if (count == max) {
prefabsInContainer = null;
return;
}
}
}
}
else {
foreach (PrefabConfig prefab in lootTable.Prefabs) {
if (prefabsInContainer.Contains(prefab.PrefabDefinition)) continue;
if (UnityEngine.Random.Range(0f, 100f) > prefab.Chance) continue;
SpawnIntoContainer(container, prefab.PrefabDefinition);
prefabsInContainer.Add(prefab.PrefabDefinition);
}
}
prefabsInContainer = null;
}
private void SpawnIntoContainer(ItemContainer container, string prefab) {
if (_allLootSpawnSlots.ContainsKey(prefab)) {
foreach (LootContainer.LootSpawnSlot lootSpawnSlot in _allLootSpawnSlots[prefab]) for (int j = 0; j < lootSpawnSlot.numberToSpawn; j++) if (UnityEngine.Random.Range(0f, 1f) <= lootSpawnSlot.probability) lootSpawnSlot.definition.SpawnIntoContainer(container);
}
else _allLootSpawn[prefab].SpawnIntoContainer(container);
}
private void AddToContainerItem(ItemContainer container, LootTableConfig lootTable) {
HashSet<int> indexMove = new HashSet<int>();
if (lootTable.UseCount) {
int count = UnityEngine.Random.Range(lootTable.Min, lootTable.Max + 1);
while (indexMove.Count < count) {
foreach (ItemConfig item in lootTable.Items) {
if (indexMove.Contains(lootTable.Items.IndexOf(item))) continue;
if (UnityEngine.Random.Range(0.0f, 100.0f) <= item.Chance) {
Item newItem = item.IsBluePrint ? ItemManager.CreateByName("blueprintbase") : ItemManager.CreateByName(item.ShortName, UnityEngine.Random.Range(item.MinAmount, item.MaxAmount + 1), item.SkinID);
if (newItem == null) {
PrintWarning($"Failed to create item! ({item.ShortName})");
continue;
}
if (item.IsBluePrint) newItem.blueprintTarget = ItemManager.FindItemDefinition(item.ShortName).itemid;
if (!string.IsNullOrEmpty(item.Name)) newItem.name = item.Name;
if (container.capacity < container.itemList.Count + 1) container.capacity++;
if (!newItem.MoveToContainer(container)) newItem.Remove();
else {
indexMove.Add(lootTable.Items.IndexOf(item));
if (indexMove.Count == count) return;
}
}
}
}
}
else {
foreach (ItemConfig item in lootTable.Items) {
if (indexMove.Contains(lootTable.Items.IndexOf(item))) continue;
if (UnityEngine.Random.Range(0.0f, 100.0f) <= item.Chance) {
Item newItem = item.IsBluePrint ? ItemManager.CreateByName("blueprintbase") : ItemManager.CreateByName(item.ShortName, UnityEngine.Random.Range(item.MinAmount, item.MaxAmount + 1), item.SkinID);
if (newItem == null) {
PrintWarning($"Failed to create item! ({item.ShortName})");
continue;
}
if (item.IsBluePrint) newItem.blueprintTarget = ItemManager.FindItemDefinition(item.ShortName).itemid;
if (!string.IsNullOrEmpty(item.Name)) newItem.name = item.Name;
if (container.capacity < container.itemList.Count + 1) container.capacity++;
if (!newItem.MoveToContainer(container)) newItem.Remove();
else indexMove.Add(lootTable.Items.IndexOf(item));
}
}
}
}
private void CheckAllLootTables() {
foreach (KeyValuePair<string, MonumentSpawnPoint> dic in _monumentSpawnPoints) {
foreach (PresetConfig preset in dic.Value.Presets) {
if (!preset.Enabled) continue;
CheckLootTable(preset.OwnLootTable);
CheckPrefabLootTable(preset.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject($"BetterNpc/Monument/{dic.Key}", dic.Value);
}
foreach (KeyValuePair<string, MonumentSpawnPoint> dic in _underwaterLabSpawnPoints) {
foreach (PresetConfig preset in dic.Value.Presets) {
if (!preset.Enabled) continue;
CheckLootTable(preset.OwnLootTable);
CheckPrefabLootTable(preset.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject($"BetterNpc/Monument/Underwater Lab/{dic.Key}", dic.Value);
}
foreach (KeyValuePair<string, MonumentSpawnPoint> dic in _tunnelSpawnPoints) {
foreach (PresetConfig preset in dic.Value.Presets) {
if (!preset.Enabled) continue;
CheckLootTable(preset.OwnLootTable);
CheckPrefabLootTable(preset.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject($"BetterNpc/Monument/Tunnel/{dic.Key}", dic.Value);
}
foreach (KeyValuePair<string, CustomSpawnPoint> dic in _customSpawnPoints) {
foreach (PresetConfig preset in dic.Value.Presets) {
if (!preset.Enabled) continue;
CheckLootTable(preset.OwnLootTable);
CheckPrefabLootTable(preset.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject($"BetterNpc/Custom/{dic.Key}", dic.Value);
}
foreach (KeyValuePair<string, EventSpawnPoint> dic in _eventSpawnPoints) {
foreach (PresetConfig preset in dic.Value.Presets) {
if (!preset.Enabled) continue;
CheckLootTable(preset.OwnLootTable);
CheckPrefabLootTable(preset.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject($"BetterNpc/Event/{dic.Key}", dic.Value);
}
if (_cargoShipSpawnPoint != null && _cargoShipSpawnPoint.Enabled) {
if (_cargoShipSpawnPoint.NpcMovingCargo.Enabled) {
CheckLootTable(_cargoShipSpawnPoint.NpcMovingCargo.OwnLootTable);
CheckPrefabLootTable(_cargoShipSpawnPoint.NpcMovingCargo.PrefabLootTable);
}
if (_cargoShipSpawnPoint.NpcStationaryInsideCargo.Enabled) {
CheckLootTable(_cargoShipSpawnPoint.NpcStationaryInsideCargo.OwnLootTable);
CheckPrefabLootTable(_cargoShipSpawnPoint.NpcStationaryInsideCargo.PrefabLootTable);
}
if (_cargoShipSpawnPoint.NpcStationaryOutsideCargo.Enabled) {
CheckLootTable(_cargoShipSpawnPoint.NpcStationaryOutsideCargo.OwnLootTable);
CheckPrefabLootTable(_cargoShipSpawnPoint.NpcStationaryOutsideCargo.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject("BetterNpc/Event/CargoShip", _cargoShipSpawnPoint);
}
foreach (KeyValuePair<string, RoadSpawnPoint> dic in _roadSpawnPoints) {
foreach (PresetConfig preset in dic.Value.Presets) {
if (!preset.Enabled) continue;
CheckLootTable(preset.OwnLootTable);
CheckPrefabLootTable(preset.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject($"BetterNpc/Road/{dic.Key}", dic.Value);
}
foreach (KeyValuePair<string, BiomeSpawnPoint> dic in _biomeSpawnPoints) {
foreach (PresetConfig preset in dic.Value.Presets) {
if (!preset.Enabled) continue;
CheckLootTable(preset.OwnLootTable);
CheckPrefabLootTable(preset.PrefabLootTable);
}
Interface.Oxide.DataFileSystem.WriteObject($"BetterNpc/Biome/{dic.Key}", dic.Value);
}
}
private static void CheckLootTable(LootTableConfig lootTable) {
lootTable.Items = lootTable.Items.O
// ... [Code preview truncated due to length. Full source in attached file] ...