using System;
using System.Collections.Generic;
using UnityEngine;
using Oxide.Core;
using Oxide.Core.Plugins;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("Zombie Monsters", "gamezoneone", "2.3.8")]
[Description("Spawns Scarecrow NPCs in PvP zones with configurable despawn, lifetime and optional Purge event buffs.")]
public class ZombieMonsters : RustPlugin
{
[PluginReference] Plugin Purge;
[PluginReference] Plugin MonumentEvents;
private const string PermAdmin = "zombiemonsters.admin";
/// Nur Scarecrow — mehrere Kandidaten, falls sich Prefab-Pfade je Build unterscheiden.
private static readonly string[] ScarecrowPrefabCandidates =
{
"assets/prefabs/npc/scarecrow/scarecrow.prefab",
"assets/rust.ai/agents/npcplayer/humannpc/scarecrow/scarecrow.prefab"
};
/// Standard-Nahkampf nach komplettem Strip (kein Vanilla-Granaten-/Kit-Zeug).
private const string ZombieMeleeShortname = "bone.club";
private readonly Dictionary SpawnPoints = new Dictionary();
private readonly List ActiveZombies = new List();
private readonly HashSet ActiveMonumentEvents = new HashSet(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary MonumentPositions = new Dictionary(StringComparer.OrdinalIgnoreCase);
private static readonly Dictionary MonumentDisplayNames = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["radtown_1"] = "Radtown",
["gas_station_1"] = "Oxum's Gas Station",
["supermarket_1"] = "Abandoned Supermarket",
["water_treatment_plant_1"] = "Water Treatment Plant",
["powerplant_1"] = "Power Plant",
["ferry_terminal_1"] = "Ferry Terminal",
["harbor_2"] = "Small Harbor",
["harbor_1"] = "Large Harbor",
["junkyard_1"] = "Junkyard",
["sphere_tank"] = "The Dome",
["airfield_1"] = "Airfield",
["trainyard_1"] = "Train Yard",
["satellite_dish"] = "Satellite Dish",
["radtown_small_3"] = "Sewer Branch",
["launch_site_1"] = "Launch Site"
};
private float SpawnInterval = 75f;
private int ZombiesPerPoint = 2;
private float ScarecrowLifetime = 480f;
private float DespawnDistance = 60f;
private float MinAliveBeforeDistanceDespawn = 90f;
/// Keine Spawns im TC-Umkreis (Bauten / Compound).
private const float CupboardExclusionRadius = 48f;
/// Keine Spawns auf Schlafsack/Bett (Respawn).
private const float SleepingBagExclusionRadius = 20f;
/// Mindestabstand zum auslösenden Spieler (verhindert Spawns in Base am Zonenrand).
private const float MinSpawnDistanceFromPlayer = 22f;
private Timer spawnTimer;
private string T(string key, string userId = null) => lang.GetMessage(key, this, userId);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary
{
["NoPermission"] = "You don't have permission to use this command.",
["ZombiesKilled"] = "All zombies have been removed.",
["WaveTriggered"] = "Zombie wave triggered manually.",
["SpawnSuccess"] = "Spawned {0} Scarecrow(s) near you.",
["SpawnFailed"] = "Spawn failed — check server log (prefab).",
}, this);
}
void Init()
{
permission.RegisterPermission(PermAdmin, this);
}
// ---------------------------------------------------------
// INITIALISIERUNG
// ---------------------------------------------------------
void OnServerInitialized()
{
ConsoleSystem.Run(ConsoleSystem.Option.Server, "zombie.population 0");
FindAllSpawnPoints();
BuildMonumentPositionCache();
RefreshActiveMonumentEvents();
StartZombieLoop();
timer.Once(10f, SpawnWaveNearPlayers);
}
void Unload()
{
spawnTimer?.Destroy();
KillAllZombies();
}
// ---------------------------------------------------------
// SPAWNPOINTS FINDEN
// ---------------------------------------------------------
private void FindAllSpawnPoints()
{
SpawnPoints.Clear();
LoadPvpZonesFromZoneManager();
Puts($"[ZombieMonsters] Found {SpawnPoints.Count} spawn points.");
}
private void AddSpawnPoint(string key, Vector3 pos, float radius)
{
if (!SpawnPoints.ContainsKey(key))
SpawnPoints[key] = new SpawnZone { Id = key, Position = pos, Radius = Mathf.Max(1f, radius) };
}
private void LoadPvpZonesFromZoneManager()
{
try
{
var data = Interface.Oxide.DataFileSystem.ReadObject("ZoneManager/zone_data");
if (data?.Definitions == null || data.Definitions.Count == 0)
{
PrintWarning("[ZombieMonsters] Keine ZoneManager-Definitionen gefunden.");
return;
}
foreach (var zone in data.Definitions)
{
if (!IsPvpZone(zone)) continue;
if (zone.Radius <= 0f) continue;
if (!TryParseVector3(zone.Location, out Vector3 pos)) continue;
AddSpawnPoint(zone.Id ?? $"zone_{SpawnPoints.Count}", pos, zone.Radius);
}
}
catch (Exception ex)
{
PrintWarning($"[ZombieMonsters] Konnte ZoneManager/zone_data nicht laden: {ex.Message}");
}
}
private static bool IsPvpZone(ZoneDefinition zone)
{
if (zone == null) return false;
string id = (zone.Id ?? string.Empty).ToLowerInvariant();
string name = (zone.Name ?? string.Empty).ToLowerInvariant();
string enter = (zone.EnterMessage ?? string.Empty).ToLowerInvariant();
// Id/Name: z. B. airfield_pvp. EnterMessage: früher nur "pvp zone" (Leerzeichen) —
// deutsche Texte nutzen "PvP-Zone" → lower "pvp-zone", daher allgemein "pvp" in der Nachricht.
return id.Contains("pvp") || name.Contains("pvp") || enter.Contains("pvp");
}
private static bool TryParseVector3(string input, out Vector3 result)
{
result = Vector3.zero;
if (string.IsNullOrWhiteSpace(input)) return false;
string[] parts = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 3) return false;
if (!float.TryParse(parts[0], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float x)) return false;
if (!float.TryParse(parts[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float y)) return false;
if (!float.TryParse(parts[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float z)) return false;
result = new Vector3(x, y, z);
return true;
}
private class SpawnZone
{
public string Id;
public Vector3 Position;
public float Radius;
}
// ---------------------------------------------------------
// SPAWN LOOP
// ---------------------------------------------------------
private void StartZombieLoop()
{
spawnTimer = timer.Every(SpawnInterval, () =>
{
SpawnWaveNearPlayers();
CleanupZombies();
});
}
private void SpawnWaveNearPlayers()
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsConnected || player.IsDead())
continue;
if (!TryGetPvpZoneForPlayer(player, out var zone))
continue;
if (ShouldBlockSpawnForMomentumEvent(player.transform.position, zone))
continue;
// Spawns an zufaelligen Punkten IN der PvP-Zone (Zentrum + Radius), nicht um die Spielerposition.
// Sonst landen Zombies in Basen / auf Bags, wenn die Zone die Base nur streift.
SpawnWaveForPlayerInZone(player, zone);
}
}
private bool ShouldBlockSpawnForMomentumEvent(Vector3 playerPos, SpawnZone zone)
{
if (zone == null || ActiveMonumentEvents.Count == 0)
return false;
foreach (string monumentId in ActiveMonumentEvents)
{
if (ZoneIdMatchesMonument(zone.Id, monumentId))
return true;
if (MonumentPositions.TryGetValue(monumentId, out Vector3 monumentPos))
{
float zoneToMonument = Vector3.Distance(zone.Position, monumentPos);
if (zoneToMonument <= zone.Radius + 80f)
return true;
float playerToMonument = Vector3.Distance(playerPos, monumentPos);
if (playerToMonument <= 120f)
return true;
}
}
return false;
}
private static bool ZoneIdMatchesMonument(string zoneId, string monumentId)
{
if (string.IsNullOrWhiteSpace(zoneId) || string.IsNullOrWhiteSpace(monumentId))
return false;
if (zoneId.IndexOf(monumentId, StringComparison.OrdinalIgnoreCase) >= 0)
return true;
string baseId = TrimNumericSuffix(monumentId);
return !string.IsNullOrEmpty(baseId) &&
zoneId.IndexOf(baseId, StringComparison.OrdinalIgnoreCase) >= 0;
}
private static string TrimNumericSuffix(string monumentId)
{
int lastUnderscore = monumentId.LastIndexOf('_');
if (lastUnderscore <= 0 || lastUnderscore >= monumentId.Length - 1)
return monumentId;
string suffix = monumentId.Substring(lastUnderscore + 1);
if (int.TryParse(suffix, out _))
return monumentId.Substring(0, lastUnderscore);
return monumentId;
}
private void BuildMonumentPositionCache()
{
MonumentPositions.Clear();
List monuments = TerrainMeta.Path?.Monuments;
if (monuments == null || monuments.Count == 0)
return;
foreach (var pair in MonumentDisplayNames)
{
MonumentInfo match = FindMonumentByDisplayName(monuments, pair.Value);
if (match != null)
MonumentPositions[pair.Key] = match.transform.position;
}
}
private static MonumentInfo FindMonumentByDisplayName(List monuments, string targetName)
{
if (string.IsNullOrWhiteSpace(targetName))
return null;
foreach (MonumentInfo monument in monuments)
{
string englishName = monument?.displayPhrase?.english;
if (string.IsNullOrWhiteSpace(englishName))
continue;
if (englishName.Equals(targetName, StringComparison.OrdinalIgnoreCase))
return monument;
}
foreach (MonumentInfo monument in monuments)
{
string englishName = monument?.displayPhrase?.english;
if (string.IsNullOrWhiteSpace(englishName))
continue;
if (englishName.IndexOf(targetName, StringComparison.OrdinalIgnoreCase) >= 0 ||
targetName.IndexOf(englishName, StringComparison.OrdinalIgnoreCase) >= 0)
return monument;
}
return null;
}
private void RefreshActiveMonumentEvents()
{
ActiveMonumentEvents.Clear();
if (MonumentEvents == null)
return;
foreach (string monumentId in MonumentDisplayNames.Keys)
{
object result = MonumentEvents.Call("IsMonumentEventsRunning", monumentId);
if (result is bool isRunning && isRunning)
ActiveMonumentEvents.Add(monumentId);
}
}
private void OnMEStarted(string monumentId)
{
if (!string.IsNullOrWhiteSpace(monumentId))
ActiveMonumentEvents.Add(monumentId);
}
private void OnMEStop(string monumentId)
{
if (!string.IsNullOrWhiteSpace(monumentId))
ActiveMonumentEvents.Remove(monumentId);
}
private bool TryGetPvpZoneForPlayer(BasePlayer player, out SpawnZone zone)
{
zone = null;
if (player == null) return false;
Vector3 pos = player.transform.position;
float bestDist = float.MaxValue;
foreach (var entry in SpawnPoints)
{
var candidate = entry.Value;
float dist = Vector3.Distance(pos, candidate.Position);
if (dist > candidate.Radius + 12f)
continue;
if (dist < bestDist)
{
bestDist = dist;
zone = candidate;
}
}
return zone != null;
}
/// Wave-Spawns nur in der Zone, mit Abstand zum Spieler und ohne TC/Bags.
private int SpawnWaveForPlayerInZone(BasePlayer player, SpawnZone zone)
{
if (GameManager.server == null || TerrainMeta.HeightMap == null)
return 0;
if (player == null || zone == null)
return 0;
int spawned = 0;
int count = CountZombiesNear(zone.Position);
int max = IsPurgeActive() ? ZombiesPerPoint * 2 : ZombiesPerPoint;
if (count >= max)
return 0;
int toSpawn = max - count;
Vector3 playerPos = player.transform.position;
for (int i = 0; i < toSpawn; i++)
{
if (!TryPickRandomSpawnInPvpZone(zone, playerPos, out Vector3 flatXZ))
continue;
if (TrySpawnOneScarecrow(flatXZ, true, out _, out Vector3 spawnPos))
{
Puts($"[ZombieMonsters] Wave-Spawn @ {spawnPos}");
spawned++;
}
}
return spawned;
}
/// Zufaelliger Punkt in der Zone (XZ), mit Mindestabstand zum Spieler und ohne TC/SleepingBag.
private bool TryPickRandomSpawnInPvpZone(SpawnZone zone, Vector3 playerPos, out Vector3 flatXZ)
{
flatXZ = default;
float maxRad = Mathf.Max(8f, zone.Radius * 0.9f);
float minFromPlayer = Mathf.Min(MinSpawnDistanceFromPlayer, zone.Radius * 0.45f);
minFromPlayer = Mathf.Clamp(minFromPlayer, 10f, MinSpawnDistanceFromPlayer);
for (int attempt = 0; attempt < 40; attempt++)
{
float ang = UnityEngine.Random.Range(0f, Mathf.PI * 2f);
float rad = UnityEngine.Random.Range(6f, maxRad);
Vector3 p = zone.Position + new Vector3(Mathf.Cos(ang) * rad, 0f, Mathf.Sin(ang) * rad);
if (Vector3.Distance(p, zone.Position) > zone.Radius + 0.5f)
continue;
Vector3 playerFlat = new Vector3(playerPos.x, 0f, playerPos.z);
Vector3 pFlat = new Vector3(p.x, 0f, p.z);
if (Vector3.Distance(pFlat, playerFlat) < minFromPlayer)
continue;
if (IsExcludedZombieSpawnArea(p))
continue;
flatXZ = p;
return true;
}
return false;
}
private bool IsExcludedZombieSpawnArea(Vector3 flatXZ)
{
if (TerrainMeta.HeightMap == null)
return true;
float terrainY = TerrainMeta.HeightMap.GetHeight(flatXZ.x, flatXZ.z);
float waterY = TerrainMeta.WaterMap != null ? TerrainMeta.WaterMap.GetHeight(flatXZ) : float.MinValue;
float y = Mathf.Max(terrainY, waterY, flatXZ.y);
var sample = new Vector3(flatXZ.x, y + 1f, flatXZ.z);
var privs = new List();
Vis.Entities(sample, CupboardExclusionRadius, privs);
if (privs.Count > 0)
return true;
var bags = new List();
Vis.Entities(sample, SleepingBagExclusionRadius, bags);
return bags.Count > 0;
}
// ---------------------------------------------------------
// SPAWNING (mit Bodenhöhen-Fix)
// ---------------------------------------------------------
/// Wenn true: mindestens ein Scarecrow direkt vor dem Spieler, unabhängig vom Zähler-Limit (Admin-/Test-Spawn).
private int SpawnZombiesAt(Vector3 pos, bool adminForceMinimumOne, float spawnSpreadRadius = 20f)
{
if (GameManager.server == null || TerrainMeta.HeightMap == null)
return 0;
int spawned = 0;
if (adminForceMinimumOne)
{
Vector3 near = pos + new Vector3(
UnityEngine.Random.Range(-2f, 2f),
0f,
UnityEngine.Random.Range(-2f, 2f)
);
if (TrySpawnOneScarecrow(near, false, out _, out Vector3 adminSpawnPos))
{
Puts($"[ZombieMonsters] Admin-Spawn erfolgreich @ {adminSpawnPos}");
spawned++;
}
else
PrintWarning("[ZombieMonsters] Admin-Spawn: kein Scarecrow konnte erzeugt werden (Prefab/Server).");
}
int count = CountZombiesNear(pos);
int max = IsPurgeActive() ? ZombiesPerPoint * 2 : ZombiesPerPoint;
if (count >= max)
return spawned;
int toSpawn = max - count;
for (int i = 0; i < toSpawn; i++)
{
Vector3 rawPos = pos + new Vector3(
UnityEngine.Random.Range(-spawnSpreadRadius, spawnSpreadRadius),
0,
UnityEngine.Random.Range(-spawnSpreadRadius, spawnSpreadRadius)
);
if (TrySpawnOneScarecrow(rawPos, true, out _, out Vector3 spawnPos))
{
Puts($"[ZombieMonsters] Wave-Spawn @ {spawnPos}");
spawned++;
}
}
return spawned;
}
/// Entspricht Murderers.cs: CreateEntity mit Quaternion + bool, sonst oft null.
private bool TrySpawnOneScarecrow(Vector3 rawPos, bool useTerrainHeight, out BaseCombatEntity zombie, out Vector3 finalSpawnPos)
{
zombie = null;
finalSpawnPos = rawPos;
if (useTerrainHeight && TerrainMeta.HeightMap == null) return false;
Vector3 spawnPos = rawPos;
if (useTerrainHeight)
{
float terrainY = TerrainMeta.HeightMap.GetHeight(rawPos.x, rawPos.z);
float waterY = TerrainMeta.WaterMap != null ? TerrainMeta.WaterMap.GetHeight(rawPos) : float.MinValue;
// Schutz gegen unterirdische Punkte: nie unter Wasser-/Terrain-Niveau spawnen.
float safeY = Mathf.Max(terrainY, waterY, rawPos.y);
spawnPos = new Vector3(rawPos.x, safeY + 1f, rawPos.z);
}
foreach (var prefab in ScarecrowPrefabCandidates)
{
BaseEntity entity = GameManager.server.CreateEntity(prefab, spawnPos, Quaternion.identity, true);
if (entity == null)
continue;
var scarecrow = entity as ScarecrowNPC;
if (scarecrow == null)
{
entity.Kill();
continue;
}
scarecrow.Spawn();
ConfigureScarecrow(scarecrow);
ActiveZombies.Add(scarecrow);
zombie = scarecrow;
finalSpawnPos = scarecrow.transform.position;
return true;
}
PrintWarning("[ZombieMonsters] Kein gueltiger Scarecrow-Prefab (alle Kandidaten fehlgeschlagen).");
return false;
}
private class ZoneDataFile
{
[JsonProperty("definitions")]
public List Definitions = new List();
}
private class ZoneDefinition
{
[JsonProperty("Id")]
public string Id;
[JsonProperty("Name")]
public string Name;
[JsonProperty("Radius")]
public float Radius;
[JsonProperty("Location")]
public string Location;
[JsonProperty("EnterMessage")]
public string EnterMessage;
}
private void ConfigureScarecrow(BaseCombatEntity zombie)
{
bool purge = IsPurgeActive();
const float baseHealth = 120f;
float purgeBonus = purge ? 1.5f : 1f;
zombie.SetMaxHealth(baseHealth * purgeBonus);
zombie.SetHealth(baseHealth * purgeBonus);
if (zombie is BaseNpc npc)
{
npc.Stats.Speed = purge ? 1.1f : 0.75f;
npc.Stats.VisionRange = 22f;
}
// Kein aggressives NavMesh-Repositioning:
// PlaceOnNavMesh(0) hat in einigen Fällen NPCs unter die Map verschoben.
if (zombie is ScarecrowNPC scare)
{
// Inventar ist oft erst nach Spawn vollständig — ein Tick später bereinigen.
// Kein inventory.Strip(): kann Scarecrow/HumanNPC in manchen Builds instabil machen.
var captured = scare;
NextTick(() => ApplyZombieLoadout(captured));
}
zombie.gameObject.AddComponent().Init(
ScarecrowLifetime,
DespawnDistance,
MinAliveBeforeDistanceDespawn
);
}
private void ApplyZombieLoadout(ScarecrowNPC npc)
{
if (npc == null || npc.IsDestroyed) return;
try
{
var inv = npc.inventory;
if (inv == null) return;
// Granaten/Waffen: typischerweise Gürtel + Rucksack. Kleidung (Wear) bleibt für Optik.
inv.containerBelt?.Clear();
inv.containerMain?.Clear();
var melee = ItemManager.CreateByName(ZombieMeleeShortname, 1, 0UL);
if (melee != null && !inv.GiveItem(melee))
melee.Remove();
npc.SendNetworkUpdate();
}
catch (Exception ex)
{
PrintWarning($"[ZombieMonsters] Loadout konnte nicht gesetzt werden: {ex.Message}");
}
}
// ---------------------------------------------------------
// CLEANUP
// ---------------------------------------------------------
private void CleanupZombies()
{
for (int i = ActiveZombies.Count - 1; i >= 0; i--)
{
BaseCombatEntity zombie = ActiveZombies[i];
if (zombie == null || zombie.IsDestroyed)
{
ActiveZombies.RemoveAt(i);
continue;
}
}
}
private int CountZombiesNear(Vector3 pos)
{
int count = 0;
foreach (var zombie in ActiveZombies)
{
if (zombie != null && !zombie.IsDestroyed && Vector3.Distance(zombie.transform.position, pos) < 40f)
count++;
}
return count;
}
// ---------------------------------------------------------
// PURGE SUPPORT
// ---------------------------------------------------------
private bool IsPurgeActive()
{
if (Purge == null)
return false;
object result = Purge.Call("IsPurgeActive");
return result is bool b && b;
}
// ---------------------------------------------------------
// ADMIN COMMANDS
// ---------------------------------------------------------
private bool HasAdminAccess(BasePlayer player)
{
return player != null && (player.IsAdmin || permission.UserHasPermission(player.UserIDString, PermAdmin));
}
[ChatCommand("killzombies")]
private void CmdKillZombies(BasePlayer player, string command, string[] args)
{
if (!HasAdminAccess(player))
{
player.ChatMessage(T("NoPermission", player.UserIDString));
return;
}
KillAllZombies();
player.ChatMessage(T("ZombiesKilled", player.UserIDString));
}
[ChatCommand("zspawnhere")]
private void CmdSpawnHere(BasePlayer player, string command, string[] args)
{
if (!HasAdminAccess(player))
{
player.ChatMessage(T("NoPermission", player.UserIDString));
return;
}
Vector3 front = player.transform.position + player.eyes.HeadForward() * 1.8f + Vector3.up * 0.15f;
int n = 0;
if (TrySpawnOneScarecrow(front, false, out _, out Vector3 spawnedPos))
{
n = 1;
Puts($"[ZombieMonsters] /zspawnhere by {player.displayName}: spawn @ {spawnedPos}");
}
else
{
n = SpawnZombiesAt(player.transform.position, true, 22f);
}
player.ChatMessage(n > 0
? string.Format(T("SpawnSuccess", player.UserIDString), n)
: T("SpawnFailed", player.UserIDString));
}
[ChatCommand("zwave")]
private void CmdWave(BasePlayer player, string command, string[] args)
{
if (!HasAdminAccess(player))
{
player.ChatMessage(T("NoPermission", player.UserIDString));
return;
}
SpawnWaveNearPlayers();
CleanupZombies();
player.ChatMessage(T("WaveTriggered", player.UserIDString));
}
[ConsoleCommand("zombies.killall")]
private void ConKillAll(ConsoleSystem.Arg arg)
{
if (arg.Connection != null)
{
var player = arg.Player();
if (!HasAdminAccess(player)) return;
}
KillAllZombies();
Puts("[ZombieMonsters] Alle Zombies wurden per Konsole entfernt.");
}
[ConsoleCommand("zombies.spawnhere")]
private void ConSpawnHere(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (!HasAdminAccess(player))
{
Puts("[ZombieMonsters] zombies.spawnhere nur als Admin-Player nutzbar.");
return;
}
Vector3 front = player.transform.position + player.eyes.HeadForward() * 1.8f + Vector3.up * 0.15f;
int n = 0;
if (TrySpawnOneScarecrow(front, false, out _, out Vector3 spawnedPos))
{
n = 1;
Puts($"[ZombieMonsters] zombies.spawnhere von {player.displayName}: Spawn @ {spawnedPos}");
}
else
{
// Fallback falls der direkte Spawnpunkt ungeeignet ist.
n = SpawnZombiesAt(player.transform.position, true, 22f);
}
Puts($"[ZombieMonsters] Spawn an Position von {player.displayName}: {n} Scarecrow(s).");
}
[ConsoleCommand("zombies.wave")]
private void ConWave(ConsoleSystem.Arg arg)
{
if (arg.Connection != null)
{
var player = arg.Player();
if (!HasAdminAccess(player)) return;
}
SpawnWaveNearPlayers();
CleanupZombies();
Puts("[ZombieMonsters] Zombie-Wave wurde per Konsole ausgelöst.");
}
// ---------------------------------------------------------
// ADMIN CONSOLE ALIASES (für Admin-Tools/Panel)
// ---------------------------------------------------------
[ConsoleCommand("zadmin.spawnhere")]
private void ConAdminSpawnHere(ConsoleSystem.Arg arg) => ConSpawnHere(arg);
[ConsoleCommand("zadmin.wave")]
private void ConAdminWave(ConsoleSystem.Arg arg) => ConWave(arg);
[ConsoleCommand("zadmin.killall")]
private void ConAdminKillAll(ConsoleSystem.Arg arg) => ConKillAll(arg);
private void KillAllZombies()
{
foreach (var npc in ActiveZombies)
{
if (npc != null && !npc.IsDestroyed)
npc.Kill();
}
ActiveZombies.Clear();
}
// ---------------------------------------------------------
// ZOMBIE LIFETIME COMPONENT
// ---------------------------------------------------------
private class ZombieLifetime : MonoBehaviour
{
private BaseCombatEntity npc;
private float lifetime;
private float despawnDistance;
private float minAliveBeforeDistanceDespawn;
private float spawnTime;
public void Init(float lifetime, float despawnDistance, float minAliveBeforeDistanceDespawn)
{
this.npc = GetComponent();
this.lifetime = lifetime;
this.despawnDistance = despawnDistance;
this.minAliveBeforeDistanceDespawn = minAliveBeforeDistanceDespawn;
this.spawnTime = Time.realtimeSinceStartup;
}
void Update()
{
if (npc == null || npc.IsDestroyed)
{
Destroy(this);
return;
}
if (Time.realtimeSinceStartup - spawnTime > lifetime)
{
npc.Kill();
Destroy(this);
return;
}
float aliveFor = Time.realtimeSinceStartup - spawnTime;
if (aliveFor < minAliveBeforeDistanceDespawn)
return;
if (BasePlayer.activePlayerList == null || BasePlayer.activePlayerList.Count == 0)
return;
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsConnected)
continue;
if (Vector3.Distance(player.transform.position, npc.transform.position) < despawnDistance)
return;
}
npc.Kill();
Destroy(this);
}
}
}
}