C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
/*
* https://github.com/DocValerian/rust-plugins
* Copyright (C) 2022 DocValerian
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.*
* You should have received a copy of the GNU General Public License
* along with this program.If not, see <http://www.gnu.org/licenses/>.*
* Questions and (limited) Support can be found in our Discord: https://discord.gg/8tkBFRjg3W
*
*/
using ConVar;
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using ProtoBuf;
using Rust;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
using static Oxide.Plugins.ZombieHunt.NpcZhuntBrain;
using Physics = UnityEngine.Physics;
using Random = Oxide.Core.Random;
using Time = UnityEngine.Time;
namespace Oxide.Plugins {
[Info("ZombieHunt", "DocValerian", "2.5.2")]
class ZombieHunt : RustPlugin {
static ZombieHunt Plugin;
[PluginReference]
private Plugin ServerRewards, Kits, TPapi, LootBoxSpawner, ZNTitleManager;
const string permUse = "zombiehunt.use";
const string permAutorec = "zombiehunt.autoloot.autorec";
private string[] recWhitelist = {
"lmg.m249", "rifle.l96", "rifle.m39", "rifle.lr300", "techparts", "cctv.camera", "targeting.computer", "supply.signal", "Jackhammer",
}
;
private string[] buffedWeapons = {
"l96.entity", "bolt_rifle.entity",
}
;
private string[] weaponList = {
"rifle.m39", "rifle.lr300", "shotgun.spas12", "shotgun.pump", "rifle.ak", "rifle.semiauto", "smg.mp5", "smg.thompson", "pistol.python", "pistol.revolver", "pistol.semiauto", "smg.2", "pistol.m92"
}
;
private Dictionary<string, string> weaponMap = new Dictionary<string, string>() {
["rifle.m39"]
= "m39.entity", ["rifle.lr300"] = "lr300.entity", ["shotgun.spas12"] = "spas12.entity", ["shotgun.pump"] = "shotgun_pump.entity", ["rifle.ak"] = "ak47u.entity", ["rifle.semiauto"] = "semi_auto_rifle.entity", ["smg.mp5"] = "mp5.entity", ["smg.thompson"] = "thompson.entity", ["pistol.python"] = "python.entity", ["pistol.revolver"] = "pistol_revolver.entity", ["pistol.semiauto"] = "pistol_semiauto.entity", ["smg.2"] = "smg.entity", ["pistol.m92"] = "m92.entity"
}
;
private string currentWeaknessShortname = "";
bool HasPermission(string id, string perm) => permission.UserHasPermission(id, perm);
#region ConfigDataLoad
static ConfigFile Cfg = new ConfigFile();
class ConfigFile {
public int ZombieEventSeconds = 600;
public int ZombieDamageRange = 130;
public int BarrelZombiePercent = 30;
public float ZombieHealth = 200f;
public int ZombieMinDmg = 5;
public int ZombieMaxDmg = 17;
}
class StoredData {
public Dictionary<string, Vector3> SpawnPoints = new Dictionary<string, Vector3>();
public int ZombieNumber = 40;
public int ZombieSpawnRadius = 110;
public float ZombieArmor = 2.5f;
public float ZombieMeleeDamage = 0.5f;
public float ZombieProjectileDamage = 0.35f;
public float ZombieBuildingDamage = 0f;
public float ZombieAccuracyPercent = 70f;
public bool WeaponProtect = true;
public int RoundWinBonus = 1500;
public int BaseRPperKill = 15;
public int MinKillCount = 30;
public int GunZombiePercent = 6;
public List<string> ZombieKitNames = new List<string>() {
"spearwolf", "evilmummy", "woodknight", "knifethug", "eokabunny"
}
;
}
StoredData storedData;
class UserData {
public List<ulong> AutoRecUsers = new List<ulong>();
public List<ulong> RegisteredHunters = new List<ulong>();
public List<ulong> BarrelUsers = new List<ulong>();
}
UserData userData;
class ScoreData {
public Dictionary<ulong, int> killCounter = new Dictionary<ulong, int>();
public Dictionary<ulong, int> winCounter = new Dictionary<ulong, int>();
}
ScoreData scoreData;
public Dictionary<ulong, NpcZhunt> currentZombieList = new Dictionary<ulong, NpcZhunt>();
private HashSet<ulong> currentCorpseList = new HashSet<ulong>();
private Timer currentUITimer;
private Timer zombieTimer;
private DateTime lastRandomSpawn;
private Vector3 currentSpawnPoint;
private int currentKills = 0;
private string currentSpawnGrid;
private Dictionary<ulong, int> roundDamageCounter = new Dictionary<ulong, int>();
private Dictionary<ulong, KeyValuePair<DateTime, Vector3>> lastPlayerPosition = new Dictionary<ulong, KeyValuePair<DateTime, Vector3>>();
private string LastWinnerName = "";
private int LastWinnerKills = 0;
private Coroutine scheduleCoroutine {
get;
set;
}
private static int groundLayer;
private static int terainLayer;
private static int BuildingLayer;
private static Vector3 Vector3Down;
private List<Rust.DamageType> allowedDamageTypes = new List<Rust.DamageType>() {
Rust.DamageType.Arrow, Rust.DamageType.Blunt, Rust.DamageType.Slash, Rust.DamageType.Stab
}
;
private List<Vector3> safeZones = new List<Vector3>();
#region Classes
public static class Coroutines {
private static Dictionary<float, YieldInstruction> mem;
public static void Clear() {
if (mem != null) {
mem.Clear();
mem = null;
}
}
public static YieldInstruction WaitForSeconds(float delay) {
if (mem == null) {
mem = new Dictionary<float, YieldInstruction>();
}
YieldInstruction yield;
if (!mem.TryGetValue(delay, out yield)) {
yield = new WaitForSeconds(delay);
mem.Add(delay, yield);
}
return yield;
}
}
#endregion
void Loaded() {
Cfg = Config.ReadObject<ConfigFile>();
Plugin = this;
permission.RegisterPermission(permUse, this);
permission.RegisterPermission(permAutorec, this);
storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>("ZombieHunt");
userData = Interface.Oxide.DataFileSystem.ReadObject<UserData>("ZombieHunt_users");
scoreData = Interface.Oxide.DataFileSystem.ReadObject<ScoreData>("ZombieHunt_scores");
SaveData();
lastRandomSpawn = DateTime.Now.AddSeconds(-1300);
currentKills = 0;
currentZombieList.Clear();
}
private void OnServerInitialized() {
if (!TPapi) {
Debug.LogError("[ZombieHunt] Error! TPapi not loaded! Please fix this error before loading ZombieHunt again.Unloading...");
Interface.Oxide.UnloadPlugin(this.Title);
return;
}
groundLayer = LayerMask.GetMask("Construction", "Terrain", "World");
terainLayer = LayerMask.GetMask("Terrain", "World", "Water");
BuildingLayer = LayerMask.GetMask("Construction");
Vector3Down = new Vector3(0f, -1f, 0f);
FindMonuments();
SetupGameTimer();
foreach (var p in BasePlayer.activePlayerList) {
if (userData.RegisteredHunters.Contains(p.userID) && !LiveUiPlayers.Contains(p)) {
LiveUiPlayers.Add(p);
}
}
initTitles();
}
void Unload() {
StopScheduleCoroutine();
SaveData();
foreach (var player in UiPlayers.ToList()) {
killUI(player);
}
foreach (var player in LiveUiPlayers.ToList()) {
killLiveUI(player);
}
}
protected override void LoadDefaultConfig() {
PrintWarning("Creating a new configuration file...");
Config.WriteObject(Cfg, true);
}
void OnPlayerDisconnected(BasePlayer player) {
killUI(player);
killLiveUI(player);
}
void SaveData() {
Interface.Oxide.DataFileSystem.WriteObject("ZombieHunt", storedData);
Interface.Oxide.DataFileSystem.WriteObject("ZombieHunt_users", userData);
Interface.Oxide.DataFileSystem.WriteObject("ZombieHunt_scores", scoreData);
}
void SaveScoreData() {
Interface.Oxide.DataFileSystem.WriteObject("ZombieHunt_scores", scoreData);
}
void SaveAdminData() {
Interface.Oxide.DataFileSystem.WriteObject("ZombieHunt", storedData);
}
void SaveUserData() {
Interface.Oxide.DataFileSystem.WriteObject("ZombieHunt_users", userData);
}
#endregion
#region CustomNPC
public bool InitializeStates(ScientistBrain customNPCBrain) => false;
public bool WantsToPopulateLoot(NpcZhunt customNpc, NPCPlayerCorpse npcplayerCorpse) => false;
public byte[] GetCustomDesign() => null;
#endregion
#region Hooks
void OnPlayerSleepEnded(BasePlayer player) {
if (userData.RegisteredHunters.Contains(player.userID) && !LiveUiPlayers.Contains(player)) {
LiveUiPlayers.Add(player);
}
}
void OnEntityKill(BaseNetworkable entity) {
if (entity is NpcZhunt && currentZombieList.ContainsKey(((BasePlayer)entity).userID)) {
//Puts("DEBUG: zombie got killed");
ulong uid = ((BasePlayer)entity).userID;
currentZombieList.Remove(uid);
currentKills++;
}
}
object OnPlayerHealthChange(BasePlayer player, float oldValue, float newValue) {
if (oldValue < newValue || !(player is NpcZhunt) || newValue == 100f || !currentZombieList.ContainsKey(player.userID)) return null;
float damage = oldValue - newValue;
if (!(player.lastAttacker is BasePlayer)) return null;
BasePlayer p = player.lastAttacker as BasePlayer;
addDamageScore(p.userID, (int)Math.Ceiling(damage));
//Puts("DEBUG: HC damage " + damage + " by "+ p + "..." + player.health);
return null;
}
object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
if (info == null) return null;
// either the target is a zombie || the initiator is
if ((entity is BasePlayer && currentZombieList.ContainsKey(((BasePlayer)entity).userID)) || (info.Initiator is BasePlayer && currentZombieList.ContainsKey(((BasePlayer)info.Initiator).userID))) {
//Puts("DEBUG: at " +entity+ " with " + info.damageTypes.GetMajorityDamageType() + " w: " +info.WeaponPrefab);
BasePlayer player = info?.Initiator as BasePlayer;
NpcZhunt zombie = info?.Initiator as NpcZhunt;
if (player == null && zombie == null) return null;
float distance = (Vector3.Distance(info.Initiator.transform.position, entity.transform.position));
if (distance > 100) {
info.damageTypes.ScaleAll(0.0f);
return true;
}
float distanceMultiplyer = 1.0f - (distance / Cfg.ZombieDamageRange);
// Player attacks
if (player != null && zombie == null) {
if (!userData.RegisteredHunters.Contains(player.userID)) {
SendReply(player, "You did not yet join the hunters.\nUse <color=orange>/zhunt join</color> to start hunting Zombies");
info.damageTypes.ScaleAll(0.0f);
return 0.0f;
}
Rust.DamageType thisDamageType = info.damageTypes.GetMajorityDamageType();
if (thisDamageType == Rust.DamageType.Explosion || !info.Weapon) {
SendReply(player, "Zombies are <color=red>immune to explosions</color>.");
info.damageTypes.ScaleAll(0.0f);
return 0.0f;
}
if (allowedDamageTypes.Contains(info.damageTypes.GetMajorityDamageType())) {
if (info.Weapon != null && info.Weapon.ShortPrefabName.Contains("jackhammer")) {
info.damageTypes.ScaleAll(0.6f);
}
else {
info.damageTypes.ScaleAll(1.7f);
}
}
else {
if (info.Weapon != null) {
if (buffedWeapons.Contains(info.Weapon.ShortPrefabName)) {
info.damageTypes.Set(Rust.DamageType.Bullet, 550f);
}
else if (info.Weapon.ShortPrefabName == weaponMap[currentWeaknessShortname]) {
if (info.Weapon.ShortPrefabName.Contains("spas12")) {
info.damageTypes.Set(Rust.DamageType.Bullet, 25f);
}
else {
info.damageTypes.Set(Rust.DamageType.Bullet, 60f);
}
}
else if (info.Weapon.ShortPrefabName.Contains("m249")) {
info.damageTypes.Set(Rust.DamageType.Bullet, 5f);
}
else if (info.Weapon.ShortPrefabName.Contains("spas12")) {
info.damageTypes.Set(Rust.DamageType.Bullet, 2f);
}
else if (info.Weapon.ShortPrefabName.Contains("shotgun_pump")) {
info.damageTypes.Set(Rust.DamageType.Bullet, 3f);
}
else {
info.damageTypes.Set(Rust.DamageType.Bullet, 8f);
}
}
}
if (player.transform.position.y - entity.transform.position.y > 1) {
//Puts("DEBUG: height-diff " + (player.transform.position.y - entity.transform.position.y));
info.damageTypes.ScaleAll(0.6f);
}
entity.lastAttacker = player;
info.damageTypes.ScaleAll(handleLastPlayerPosition(player, info.Weapon.ShortPrefabName) );
//Puts("DEBUG: damaging zombie for "+ info.damageTypes.Total() + " WITH " + info.damageTypes.GetMajorityDamageType() + " gun " + info.Weapon?.ShortPrefabName);
}
// Zombies attack
if (zombie != null) {
//no damage to innocents
if (entity is BasePlayer && !userData.RegisteredHunters.Contains(((BasePlayer)entity).userID)) {
info.damageTypes.ScaleAll(0.0f);
return true;
}
float rand = Random.Range(1, 101);
if (rand >= storedData.ZombieAccuracyPercent) return true;
//Puts("Attacking building");
if (entity is BuildingBlock || entity is DecayEntity || entity.name.Contains("shopfront.metal") || entity.name.Contains("shutter.metal.embrasure") || entity.name.Contains("door") || entity.name.Contains("rug") || entity.name.Contains("sign")) {
// protect innocents
info.damageTypes.ScaleAll(0.0f);
return true;
//Puts("DEBUG: ---- Zombie is attacking BASE with " + info.damageTypes.GetMajorityDamageType() + " w: " +info.WeaponPrefab +" for "+ info.damageTypes.Total());
}
else {
Rust.DamageType damageType = info.damageTypes.GetMajorityDamageType();
//Puts("DEBUG: +++++ Zombie is attacking PLAYER with " + info.damageTypes.GetMajorityDamageType() + " w: " +info.WeaponPrefab +" for "+ info.damageTypes.Total());
if (info.WeaponPrefab != null && info.WeaponPrefab.name == "grenade.f1.deployed") {
info.damageTypes.ScaleAll(0.4f);
}
else if (allowedDamageTypes.Contains(damageType)) {
info.damageTypes.ScaleAll(storedData.ZombieMeleeDamage);
}
else {
info.damageTypes.ScaleAll(storedData.ZombieProjectileDamage);
}
}
}
//Puts("DEBUG: damage "+ info.damageTypes.Total());
}
return null;
}
private void OnEntityDeath(BaseCombatEntity victimEntity, HitInfo hitInfo) {
// Ignore - there is no victim for some reason
if (victimEntity == null) return;
// Try to avoid error when entity was destroyed
if (victimEntity.gameObject == null) return;
NpcZhunt npc = victimEntity as NpcZhunt;
if (npc?.userID != null && currentZombieList.ContainsKey(npc.userID)) {
//Puts("NPC " + npc + "died --- rare: " + npc.inventory.containerBelt.GetSlot(0).info.rarity + " ranged: " + npc.inventory.containerBelt.GetSlot(0));
currentCorpseList.Add(npc.userID);
}
}
private object CanBuild(Planner planner, Construction entity, Construction.Target target) {
if (planner == null || entity == null) {
return null;
}
var player = planner?.GetOwnerPlayer();
if (player == null) return null;
float distance = Vector3Ex.Distance2D(player.transform.position, currentSpawnPoint);
if (distance <= (storedData.ZombieSpawnRadius + 50)) {
var priv = player.GetBuildingPrivilege();
if (priv != null && priv.IsAuthed(player)) return null;
SendReply(player, "You can't build in /zhunt area.\nWait for zombies to move on!");
return false;
}
return null;
}
void OnEntitySpawned(BaseEntity entity) //handles smoke signals, backpacks, corpses(applying kit)
{
//corpse handling
if (entity == null) return;
if (entity is NPCPlayerCorpse) {
NPCPlayerCorpse corpse = entity as NPCPlayerCorpse;
if (corpse == null) return;
if (!currentCorpseList.Contains(corpse.playerSteamID)) return;
NextTick(() => {
if (corpse == null) return;
foreach(ItemContainer c in corpse.containers) {
c.Clear();
}
corpse.Kill();
}
);
}
if (entity is DroppedItemContainer) {
DroppedItemContainer backpack = entity as DroppedItemContainer;
NextTick(() => {
if (backpack == null) return;
ulong id = backpack.playerSteamID;
if (currentCorpseList.Contains(id)) {
backpack.Kill();
currentCorpseList.Remove(id);
}
}
);
}
// no build zone at zhunt
if (entity.ShortPrefabName.Contains("cupboard.tool")) {
BasePlayer player = BasePlayer.FindByID(entity.OwnerID);
if (player == null) return;
float distance = Vector3Ex.Distance2D(entity.transform.position, currentSpawnPoint);
// CHECK for TC distance to other TCs (of player) first
if (distance <= (storedData.ZombieSpawnRadius + 50)) {
entity.Kill(BaseNetworkable.DestroyMode.None);
Item tcitm = ItemManager.CreateByName("cupboard.tool", 1);
player.GiveItem(tcitm, BaseEntity.GiveItemReason.Generic);
SendReply(player, "You can't place a TC in /zhunt area.\nWait for zombies to move on!");
return;
}
}
}
#endregion
#region Commands
[ConsoleCommand("zhunt.close")]
private void CmdClose(ConsoleSystem.Arg arg) {
var player = arg.Connection.player as BasePlayer;
if (player == null || !HasPermission(arg.Player().UserIDString, permUse)) return;
killUI(player);
}
private void showAdminMessage(BasePlayer player) {
string msg = "<color=orange>===== Zombie Hunt Admin tools ======</color>";
msg += "\nCurrent Pop: " + currentZombieList.Count();
msg += "\n\nUsage:";
msg += "\n/zadm \t\t\t\tshow this info";
msg += "\n/zadm forcemove \t\tforce moving";
msg += "\n/zadm setnum n \t\t\tset total zombie number";
msg += "\n/zadm setrad n \t\t\tset spawn radius";
msg += "\n/zadm sett2 n \t\t\tupdate t2 base";
msg += "\n/zadm sett3 n \t\t\tupdate t3 base";
msg += "\n/zadm setrpk n \t\t\tupdate RP per kill base";
msg += "\n/zadm addkit name \t\tadd kit name";
msg += "\n/zadm removekit name \tadd kit name";
msg += "\n/zadm addgunkit name \t\tadd kit name";
msg += "\n/zadm removegunkit name \tadd kit name";
msg += "\n/zadm setaccuracy x.y \tchange accuracy setting";
msg += "\n/zadm setarmor x.y \t\tChange armor setting";
msg += "\n/zadm setmelee x.y \t\tChange melee multiplier";
msg += "\n/zadm setprojectile x.y \tChange projectile multiplier";
msg += "\n/zadm setbuilding x.y \t\tChange building damage";
msg += "\n/zadm setbonus n \t\tset round win RP bonus";
SendReply(player, msg);
}
[ChatCommand("zadm")]
private void ZombieAdmin(BasePlayer player, string command, string[] args) {
if (!player.IsAdmin) return;
if (args.Length == 0) {
SendReply(player, "TEST: " + ItemManager.itemDictionaryByName[currentWeaknessShortname].displayName.english + " " + ItemManager.itemDictionaryByName[currentWeaknessShortname].name);
showAdminMessage(player);
return;
}
if (args.Length == 2) {
int num = 0;
float fnum = 0.0f;
string name = "";
object isKit;
switch (args[0]) {
case "forcemove" : lastRandomSpawn = DateTime.Now.AddSeconds(-Cfg.ZombieEventSeconds);
SendReply(player, "Forced move!");
break;
case "setbonus" : try {
num = int.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setbonus X");
return;
}
storedData.RoundWinBonus = num;
SaveAdminData();
SendReply(player, "Round Bonus set to " + num + " RP");
break;
case "addkit" : name = args[1].ToLower();
isKit = Kits?.Call("isKit", name);
if (isKit is bool && (bool)isKit && !storedData.ZombieKitNames.Contains(name)) {
storedData.ZombieKitNames.Add(name);
SaveAdminData();
SendReply(player, "Kit added to zombie kits: " + name);
}
else {
SendReply(player, "ERROR: not a valid kit: " + name);
}
break;
case "removekit" : name = args[1].ToLower();
if (storedData.ZombieKitNames.Contains(name)) {
storedData.ZombieKitNames.Remove(name);
SaveAdminData();
SendReply(player, "Kit Removed from zombie kits: " + name);
}
else {
SendReply(player, "ERROR: not a valid kit: " + name);
}
break;
case "setrpk" : try {
num = int.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setrpk X");
return;
}
storedData.BaseRPperKill = num;
SaveAdminData();
SendReply(player, "Round BaseRPperKill set to " + num);
break;
case "setrad" : try {
num = int.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setrad X");
return;
}
if (num < storedData.ZombieSpawnRadius) {
RoundEndActions();
}
storedData.ZombieSpawnRadius = num;
SaveAdminData();
SendReply(player, "Spawn Radius set to " + num);
break;
case "setnum" : try {
num = int.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setnum X");
return;
}
if (num < storedData.ZombieNumber) {
RoundEndActions();
}
storedData.ZombieNumber = num;
SaveAdminData();
SendReply(player, "Zombie number set to " + num);
break;
case "setgunpct" : try {
num = int.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setgunpct X");
return;
}
if (num < storedData.GunZombiePercent) {
RoundEndActions();
}
storedData.GunZombiePercent = num;
SaveAdminData();
SendReply(player, "Gun Zombie percent set to " + num);
break;
case "setspawn" : name = args[1].ToLower();
Vector3 pos = player.transform.position;
if (storedData.SpawnPoints.ContainsKey(name)) {
storedData.SpawnPoints[name] = pos;
}
else {
storedData.SpawnPoints.Add(name, pos);
}
SendReply(player, "Set spawnpoint: " + name + " at " + pos);
SaveAdminData();
break;
case "setaccuracy" : try {
fnum = float.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setaccurary X.Y");
return;
}
storedData.ZombieAccuracyPercent = fnum;
SaveAdminData();
SendReply(player, "Zombie accuracy % set to " + fnum);
break;
case "setarmor" : try {
fnum = float.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setarmor X.Y");
return;
}
storedData.ZombieArmor = fnum;
SaveAdminData();
SendReply(player, "Zombie armor set to " + fnum);
break;
case "setbuilding" : try {
fnum = float.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setbuilding X.Y");
return;
}
storedData.ZombieBuildingDamage = fnum;
SaveAdminData();
SendReply(player, "Zombie building damage set to " + fnum);
break;
case "setmelee" : try {
fnum = float.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setmelee X.Y");
return;
}
storedData.ZombieMeleeDamage = fnum;
SaveAdminData();
SendReply(player, "Zombie melee multiplier set to " + fnum);
break;
case "setprojectile" : try {
fnum = float.Parse(args[1]);
}
catch (FormatException) {
SendReply(player, "Use /zadm setprojectile X.Y");
return;
}
storedData.ZombieProjectileDamage = fnum;
SaveAdminData();
SendReply(player, "Zombie projectile multiplier set to " + fnum);
break;
default : showAdminMessage(player);
break;
}
}
}
[ChatCommand("zscore")]
private void ZhuntScoreCommand(BasePlayer player, string command, string[] args) {
reloadUI(player);
}
private void showUsageMessage(BasePlayer player) {
string msg = "<color=orange>===== Zombie Hunt (v." + Plugin.Version + " by " + Plugin.Author + ") ======</color>";
msg += "\nZombies only attack players that JOINed as hunters!";
msg += "\n'Kills' are based on damage done to prevent kill-stealing";
msg += "\n\n<color=green>Usage:</color>";
msg += "\n/zhunt \t\t\tshow this info";
msg += "\n/zscore \t\t\tshow Highscores";
msg += "\n/zhunt join\t\t\tJoin the hunt";
msg += "\n/zhunt leave\t\tBe safe from Zombies";
msg += "\n/zhunt where\t\tshow the zombie location and timer";
msg += "\n/zhunt tp\t\t\tteleport into the zombie area";
msg += "\n\n<color=green>Loot & RP:</color>";
msg += "\n/loot \t\t\t\tAfter the round: \n\t\t\t\t - spawn crates with all your loot!\n\t\t\t\t - RP + 1-4 items (based on # kills)";
SendReply(player, msg);
}
[ChatCommand("zhunt")]
private void CmdZhunt(BasePlayer player, string command, string[] args) {
if (args.Length == 0) {
showUsageMessage(player);
return;
}
string msg = "";
switch (args[0]) {
case "join" : if (!userData.RegisteredHunters.Contains(player.userID)) {
userData.RegisteredHunters.Add(player.userID);
msg += "You are now a licensed <color=green>Zombie Hunter</color>.Happy hunting!";
SendReply(player, msg);
if (!LiveUiPlayers.Contains(player)) {
LiveUiPlayers.Add(player);
}
}
else {
msg += "You did already join the hunters.\nUse <color=orange>/zhunt leave</color> to leave and be safe from Zombies";
SendReply(player, msg);
}
SaveUserData();
break;
case "leave" : if (getKillsForPlayer(player.userID) == 0) {
if (userData.RegisteredHunters.Contains(player.userID)) {
userData.RegisteredHunters.Remove(player.userID);
killLiveUI(player);
roundDamageCounter.Remove(player.userID);
lastPlayerPosition.Remove(player.userID);
msg += "You are now <color=green>safe</color>.\nRound progress deleted!";
SendReply(player, msg);
}
else {
msg += "You did not yet join the hunters.\nUse <color=orange>/zhunt join</color> to start hunting Zombies";
SendReply(player, msg);
}
SaveUserData();
}
else {
SendReply(player, "<color=red>WARNING:</color> this will delete your round-progress!\nUse <color=orange>/zhunt leavenow</color> to actually leave!");
}
break;
case "leavenow" : if (userData.RegisteredHunters.Contains(player.userID)) {
userData.RegisteredHunters.Remove(player.userID);
killLiveUI(player);
roundDamageCounter.Remove(player.userID);
lastPlayerPosition.Remove(player.userID);
msg += "You are now <color=green>safe</color>.\nRound progress deleted!";
SendReply(player, msg);
}
else {
msg += "You did not yet join the hunters.\nUse <color=orange>/zhunt join</color> to start hunting Zombies";
SendReply(player, msg);
}
SaveUserData();
break;
case "where" : msg += "Zombies are currently in grid <color=green>" + currentSpawnGrid + "</color>";
msg += "\nThey will move in <color=orange>" + Math.Ceiling(Cfg.ZombieEventSeconds - (DateTime.Now - lastRandomSpawn).TotalSeconds) + " seconds</color>";
SendReply(player, msg);
break;
case "tp" : float distance = Vector3Ex.Distance2D(player.transform.position, currentSpawnPoint);
if (distance <= (storedData.ZombieSpawnRadius + 50)) {
SendReply(player, "You are already in the ZHunt area!");
break;
}
else {
TPapi?.Call("TeleportPlayerTo", player, GetNewPosition());
SendReply(player, "Teleported to Zombie infested area!");
}
break;
default : showUsageMessage(player);
break;
}
}
#endregion
#region Spawning
internal NpcZhunt SpawnNPC(Vector3 pos) {
pos.y = TerrainMeta.HeightMap.GetHeight(pos);
object position = FindPointOnNavmesh(pos, 10f);
if (position is Vector3 && (Vector3)position != Vector3.zero) {
NpcZhunt scientistNPC = InstantiateEntity((Vector3)position, Quaternion.Euler(0, 0, 0));
if (scientistNPC == null) return null;
scientistNPC.enableSaving = false;
scientistNPC.Spawn();
// _allNPC.Add(scientistNPC);
scientistNPC.gameObject.SetActive(true);
NextTick(() => {
scientistNPC.InitNewSettings((Vector3)position, Cfg.ZombieHealth);
if (storedData.ZombieKitNames.Count > 0) scientistNPC.setUpGear(storedData.ZombieKitNames.GetRandom());
else scientistNPC.setUpGear("");
}
);
scientistNPC.displayName = "Zhunt Zombie";
if (scientistNPC != null) return scientistNPC;
}
return null;
}
private static NpcZhunt InstantiateEntity(Vector3 position, Quaternion rotation) {
GameObject gameObject = Instantiate.GameObject(GameManager.server.FindPrefab("assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_any.prefab"), position, Quaternion.identity);
gameObject.name = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_full_any.prefab";
gameObject.SetActive(false);
ScientistNPC scientistNPC = gameObject.GetComponent<ScientistNPC>();
ScientistBrain defaultBrain = gameObject.GetComponent<ScientistBrain>();
defaultBrain._baseEntity = scientistNPC;
NpcZhunt component = gameObject.AddComponent<NpcZhunt>();
NpcZhuntBrain brains = gameObject.AddComponent<NpcZhuntBrain>();
brains.Pet = false;
brains.AllowedToSleep = false;
CopyFields<NPCPlayer>(scientistNPC, component);
brains._baseEntity = component;
UnityEngine.Object.DestroyImmediate(defaultBrain, true);
UnityEngine.Object.DestroyImmediate(scientistNPC, true);
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(gameObject, Rust.Server.EntityScene);
UnityEngine.Object.Destroy(gameObject.GetComponent<Spawnable>());
return component;
}
private static void CopyFields<T>(T src, T dst) {
var fields = typeof(T).GetFields();
foreach (var field in fields) {
if (field.IsStatic) continue;
object value = field.GetValue(src);
field.SetValue(dst, value);
}
}
private static List<Vector3> GetCircumferencePositions(Vector3 center, float radius, float next, float y) // Thanks to ArenaWallGenerator
{
var positions = new List<Vector3>();
float degree = 0f;
while (degree < 360) {
float angle = (float)(2 * Math.PI / 360) * degree;
float x = center.x + radius * (float)Math.Cos(angle);
float z = center.z + radius * (float)Math.Sin(angle);
var position = new Vector3(x, center.y, z);
object success = FindPointOnNavmesh(position, 10f);
if (success != null) positions.Add((Vector3)success);
degree += next;
}
return positions;
}
private static NavMeshHit navmeshHit;
private static RaycastHit raycastHit;
private static Collider[] _buffer = new Collider[256];
private const int WORLD_LAYER = 65536;
internal static object FindPointOnNavmesh(Vector3 targetPosition, float maxDistance = 4f) {
for (int i = 0; i < 10; i++) {
Vector3 position = i == 0 ? targetPosition : targetPosition + (UnityEngine.Random.onUnitSphere * maxDistance);
if (NavMesh.SamplePosition(position, out navmeshHit, maxDistance, 1)) {
if (IsInRockPrefab2(navmeshHit.position)) continue;
if (IsInRockPrefab(navmeshHit.position)) continue;
if (IsNearWorldCollider(navmeshHit.position)) continue;
return navmeshHit.position;
}
}
return null;
}
internal static bool IsInRockPrefab(Vector3 position) {
Physics.queriesHitBackfaces = true;
bool isInRock = Physics.Raycast(position, Vector3.up, out raycastHit, 20f, WORLD_LAYER, QueryTriggerInteraction.Ignore) && blockedColliders.Any(s => raycastHit.collider?.gameObject?.name.Contains(s) ?? false);
Physics.queriesHitBackfaces = false;
return isInRock;
}
internal static bool IsInRockPrefab2(Vector3 position) {
Vector3 p1 = position + new Vector3(0, 20f, 0);
Vector3 p2 = position + new Vector3(0, -2f, 0);
Vector3 diff = p2 - p1;
RaycastHit[] hits;
hits = Physics.RaycastAll(p1, diff, diff.magnitude);
if (hits.Length > 0) {
for (int i = 0; i < hits.Length; i++) {
RaycastHit hit = hits[i];
if (hit.collider != null) {
bool isRock = blockedColliders.Any(s => hit.collider?.gameObject?.name.ToLower().Contains(s) ?? false);
if (isRock) return true;
}
}
}
return false;
}
internal static bool IsNearWorldCollider(Vector3 position) {
Physics.queriesHitBackfaces = true;
int count = Physics.OverlapSphereNonAlloc(position, 2f, _buffer, WORLD_LAYER, QueryTriggerInteraction.Ignore);
Physics.queriesHitBackfaces = false;
int removed = 0;
for (int i = 0; i < count; i++) {
if (acceptedColliders.Any(s => _buffer[i].gameObject.name.Contains(s))) removed++;
}
return count - removed > 0;
}
private static readonly string[] acceptedColliders = new string[] {
"road", "carpark", "rocket_factory", "range", "train_track", "runway", "_grounds", "concrete_slabs", "lighthouse", "cave", "office", "walkways", "sphere", "tunnel", "industrial", "junkyard"
}
;
private static readonly string[] blockedColliders = new string[] {
"rock", "junk", "range", "invisible", "cliff", "prevent_movement", "formation_"
}
;
#endregion
#region NpcZhunt
public class NpcZhunt {
// This code is removed, since I didn't write it myself.To make it work, find an AI class or refactor to use chaos NPCs
}
#endregion
#region Brain
public class NpcZhuntBrain {
// This code is removed, since I didn't write it myself.To make it work, find an AI Brain class or refactor to use chaos NPCs
}
#endregion
#region Functions
private void initTitles() {
ZNTitleManager?.Call("UpsertTitle", getTitleID(), "Z-Hunt", "Zombies", "Legendary Z-Hunter", "Legendary Z-Huntress", "ZombieHunter", "ZombieHuntress");
}
private string getTitleID() {
return Plugin.Name + "_zhunt";
}
private void TMP_SimulateAction() {
roundDamageCounter.Add(24235235, 2900);
roundDamageCounter.Add(95643, 100);
roundDamageCounter.Add(95234643, 440);
roundDamageCounter.Add(9563443, 3000);
roundDamageCounter.Add(95673443, 4800);
currentKills = 112;
}
private float handleLastPlayerPosition(BasePlayer player, string gun = "") {
if (lastPlayerPosition.ContainsKey(player.userID)) {
if(lastPlayerPosition[player.userID].Key < DateTime.Now.AddSeconds(-30)) {
//Puts("DEBUG: Camping for " + (DateTime.Now - lastPlayerPosition[player.userID].Key) + "s");
if (Vector3Ex.Distance2D(lastPlayerPosition[player.userID].Value, player.transform.position) <= 15) {
//Puts("DEBUG: Didn't move! only" + Vector3Ex.Distance2D(lastPlayerPosition[player.userID].Value, player.transform.position) + "m");
SendReply(player, "<color=red>WARNING!</color> Attacking from your current location will reduce damage to 5% (anti camping)." );
return 0.05f;
}
else {
//update location data
lastPlayerPosition[player.userID] = new KeyValuePair<DateTime, Vector3>(DateTime.Now, player.transform.position);
return 1.0f;
}
}
}
else {
lastPlayerPosition.Add(player.userID, new KeyValuePair<DateTime, Vector3>(DateTime.Now, player.transform.position));
return 1.0f;
}
return 1.0f;
}
private int getKillsFromDamage(int damage) {
return (int)Math.Floor(damage / Cfg.ZombieHealth);
}
private int getKillsForPlayer(ulong playerID) {
return roundDamageCounter.ContainsKey(playerID) ? getKillsFromDamage(roundDamageCounter[playerID]) : 0;
}
private float getKillPercent(ulong playerID) {
if (roundDamageCounter.Count() == 0) return 0f;
int playerDamage = roundDamageCounter.ContainsKey(playerID) ? roundDamageCounter[playerID] : 0;
float totalDamage = (float)roundDamageCounter.Sum(x => x.Value);
float percent = (float)(playerDamage / totalDamage) * 100f;
return (float)Math.Round(percent, 2);
}
private int getRoundBonus() {
if (roundDamageCounter.Count() == 0) return 0;
int roundAboveMinPlayers = -1;
int minDamage = storedData.MinKillCount * 100;
foreach (int dmg in roundDamageCounter.Values) {
if (dmg >= minDamage) roundAboveMinPlayers++;
}
int realBonus = (roundAboveMinPlayers > 0) ? storedData.RoundWinBonus * roundAboveMinPlayers : 0;
return realBonus;
}
private int getRP(ulong playerID) {
int kills = getKillsForPlayer(playerID);
if (kills == 0) return 0;
float killPercent = getKillPercent(playerID) / 100f;
int killRP = kills * storedData.BaseRPperKill;
int roundRP = 0;
if (kills >= storedData.MinKillCount) {
roundRP = (int)Math.Ceiling(getRoundBonus() * killPercent);
}
//Puts("DEBUG: dd k:" + killRP + " r:" + roundRP + " a:" + killPercent);
return killRP + roundRP;
}
private Dictionary<string, float> getStatDict(ulong playerID) {
if (currentKills == 0) {
return new D
// ... [Code preview truncated due to length. Full source in attached file] ...