C#:
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("SharkAttack", "DAKARAT", "1.0.0")]
[Description("Event: a pack of sharks invades the waters around your players. Full despawn when it ends.")]
public class SharkAttack : RustPlugin
{
// --------------------------------------------------------------------
// Sharks are spawned near players first and anywhere deep enough as a
// fallback, so the event still fires on an empty or land-heavy map.
// Every spawned shark is tracked and killed when the event stops or the
// plugin unloads - an event that leaves creatures behind is worse than
// no event at all.
// --------------------------------------------------------------------
private const string PermAdmin = "sharkattack.admin";
private bool _active;
private readonly List<BaseEntity> _sharks = new List<BaseEntity>();
#region Configuration
private Configuration _config;
private class Configuration
{
// simpleshark.prefab is the real shark agent. The obvious-looking
// ".../shark/shark.prefab" simply does not exist in the game files.
[JsonProperty("Shark prefab")]
public string SharkPrefab = "assets/rust.ai/agents/fish/simpleshark.prefab";
[JsonProperty("Number of sharks to spawn")] public int TotalSharks = 15;
[JsonProperty("Spawn radius around players (m)")] public float SpawnRadius = 60f;
[JsonProperty("Minimum water depth to spawn (m)")] public float MinWaterDepth = 1.5f;
[JsonProperty("Chat prefix")] public string Prefix = "<color=#1c6ea4>[Sharks]</color>";
}
protected override void LoadDefaultConfig() => _config = new Configuration();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null) LoadDefaultConfig();
}
catch
{
PrintWarning("Configuration file is invalid, loading defaults.");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config);
#endregion
#region Localisation
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "You are not allowed to use this command.",
["Started"] = "<color=#ce422b>SHARK ATTACK!</color> Sharks are flooding the waters - <color=#ffffff>get out of the sea!</color>",
["Ended"] = "The sharks return to the deep. The water is safe... for now.",
["NoWater"] = "...the sea is too calm, no shark found anywhere to spawn.",
["AlreadyActive"] = "A shark attack is already running.",
["Usage"] = "Usage: /sharks [start|stop]"
}, this);
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "Tu n'as pas la permission d'utiliser cette commande.",
["Started"] = "<color=#ce422b>LES DENTS DE LA MER !</color> Des requins envahissent les eaux - <color=#ffffff>sortez de l'eau !</color>",
["Ended"] = "Les requins regagnent les profondeurs. L'eau est sure... pour l'instant.",
["NoWater"] = "...l'eau est trop calme, aucun requin n'a trouve ou apparaitre.",
["AlreadyActive"] = "Une attaque de requins est deja en cours.",
["Usage"] = "Usage : /sharks [start|stop]"
}, this, "fr");
}
private string L(string key, BasePlayer player = null)
=> lang.GetMessage(key, this, player == null ? null : player.UserIDString);
#endregion
#region Lifecycle
private void Init() => permission.RegisterPermission(PermAdmin, this);
// Safety: never leave sharks swimming around after a reload.
private void Unload() => StopEvent(false);
#endregion
#region Commands and API
[ChatCommand("sharks")]
private void CmdSharks(BasePlayer player, string cmd, string[] args)
{
if (player == null) return;
if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
PrintToChat(player, $"{_config.Prefix} {L("NoPermission", player)}");
return;
}
string sub = args != null && args.Length > 0 ? args[0].ToLower() : "start";
switch (sub)
{
case "stop":
StopEvent(true);
break;
case "start":
if (_active) PrintToChat(player, $"{_config.Prefix} {L("AlreadyActive", player)}");
else StartEvent();
break;
default:
PrintToChat(player, $"{_config.Prefix} {L("Usage", player)}");
break;
}
}
[ConsoleCommand("sharks")]
private void ConsoleSharks(ConsoleSystem.Arg arg)
{
if (arg.Player() != null && !arg.Player().IsAdmin) return;
string sub = arg.HasArgs(1) ? arg.GetString(0).ToLower() : "start";
if (sub == "stop") StopEvent(true); else StartEvent();
}
// Public API for other plugins.
private void StartSharkAttack() => StartEvent();
private void StopSharkAttack() => StopEvent(true);
private bool IsSharkAttackActive() => _active;
#endregion
#region Event
private void StartEvent()
{
if (_active) return;
_active = true;
_sharks.Clear();
Announce("Started");
int target = Mathf.Max(1, _config.TotalSharks);
int spawned = 0, attempts = 0;
// Bounded retries: on a map with very little water we must give up
// rather than loop forever looking for a valid spot.
while (spawned < target && attempts < target * 25)
{
attempts++;
Vector3 pos;
if (TryFindWaterSpawn(out pos) && SpawnShark(pos)) spawned++;
}
Puts($"SharkAttack: {spawned}/{target} sharks spawned.");
if (spawned == 0)
{
Announce("NoWater");
_active = false;
return;
}
Interface.CallHook("OnSharkAttackStarted", spawned);
}
private void StopEvent(bool announce)
{
if (!_active) return;
_active = false;
int killed = 0;
foreach (var shark in _sharks)
if (shark != null && !shark.IsDestroyed) { shark.Kill(); killed++; }
_sharks.Clear();
if (announce) Announce("Ended");
Puts($"SharkAttack: event stopped, {killed} sharks despawned.");
Interface.CallHook("OnSharkAttackEnded", killed);
}
private bool TryFindWaterSpawn(out Vector3 pos)
{
pos = Vector3.zero;
if (TerrainMeta.HeightMap == null) return false;
var players = BasePlayer.activePlayerList;
// Prefer water near a random player, so the event happens where
// someone can actually see it.
if (players.Count > 0)
{
var p = players[UnityEngine.Random.Range(0, players.Count)];
if (p != null)
{
Vector2 offset = UnityEngine.Random.insideUnitCircle * Mathf.Max(10f, _config.SpawnRadius);
Vector3 candidate = p.transform.position + new Vector3(offset.x, 0f, offset.y);
if (IsWater(candidate)) { candidate.y = -1.5f; pos = candidate; return true; }
}
}
// Fallback: anywhere deep enough on the map.
float half = TerrainMeta.Size.x * 0.45f;
var random = new Vector3(UnityEngine.Random.Range(-half, half), -1.5f, UnityEngine.Random.Range(-half, half));
if (IsWater(random)) { pos = random; return true; }
return false;
}
private bool IsWater(Vector3 pos)
{
float height = TerrainMeta.HeightMap.GetHeight(pos);
return height < -Mathf.Max(0.5f, _config.MinWaterDepth);
}
private bool SpawnShark(Vector3 pos)
{
var ent = GameManager.server.CreateEntity(_config.SharkPrefab, pos, Quaternion.identity);
if (ent == null)
{
PrintWarning($"Shark prefab not found: {_config.SharkPrefab}");
return false;
}
ent.Spawn();
_sharks.Add(ent);
return true;
}
private void Announce(string key)
{
foreach (var p in BasePlayer.activePlayerList)
if (p != null) PrintToChat(p, $"{_config.Prefix} {L(key, p)}");
}
#endregion
}
}