C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries;
namespace Oxide.Plugins
{
[Info("Weapon Kill Damage Bonus", "sonza", "1.0.0")]
[Description("Tracks kills per weapon and increases damage by 2% for every 1000 kills with that weapon.")]
public class WeaponKillDamageBonus : RustPlugin
{
private const string PermAdmin = "weaponkilldamagebonus.admin";
private PluginConfig _config;
private StoredData _data;
#region Config
private class PluginConfig
{
[JsonProperty("Kills required per damage tier")]
public int KillsPerTier = 1000;
[JsonProperty("Damage bonus per tier (0.02 = 2%)")]
public float DamageBonusPerTier = 0.02f;
[JsonProperty("Maximum damage bonus tiers (0 = unlimited)")]
public int MaxTiers = 0;
[JsonProperty("Only count player vs player kills")]
public bool PlayerKillsOnly = true;
[JsonProperty("Count scientist kills toward weapon bonuses")]
public bool TrackScientistKills = true;
[JsonProperty("Weapon shortnames excluded from tracking (optional)")]
public List<string> ExcludedWeapons = new List<string>();
[JsonProperty("Discord webhook enabled")]
public bool DiscordWebhookEnabled = true;
[JsonProperty("Discord webhook URL")]
public string DiscordWebhookUrl = "";
[JsonProperty("Discord webhook username")]
public string DiscordWebhookUsername = "Weapon Kill Tracker";
[JsonProperty("Discord embed color (decimal)")]
public int DiscordEmbedColor = 3447003;
[JsonProperty("Notify player in chat when milestone is reached")]
public bool NotifyPlayerInChat = true;
}
protected override void LoadDefaultConfig()
{
_config = new PluginConfig();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<PluginConfig>();
if (_config == null)
{
throw new JsonException("Config file was empty.");
}
}
catch
{
PrintWarning("Config invalid or missing. Creating a new default config.");
LoadDefaultConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(_config, true);
#endregion
#region Data
private class StoredData
{
public Dictionary<ulong, Dictionary<string, int>> PlayerWeaponKills = new Dictionary<ulong, Dictionary<string, int>>();
}
private void LoadData()
{
try
{
_data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
}
catch
{
_data = new StoredData();
}
}
private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, _data);
private int GetKillCount(ulong playerId, string weaponShortname)
{
if (!_data.PlayerWeaponKills.TryGetValue(playerId, out Dictionary<string, int> weapons))
{
return 0;
}
return weapons.TryGetValue(weaponShortname, out int kills) ? kills : 0;
}
private int AddKill(ulong playerId, string weaponShortname)
{
if (string.IsNullOrEmpty(weaponShortname))
{
return 0;
}
if (!_data.PlayerWeaponKills.TryGetValue(playerId, out Dictionary<string, int> weapons))
{
weapons = new Dictionary<string, int>();
_data.PlayerWeaponKills[playerId] = weapons;
}
if (!weapons.TryGetValue(weaponShortname, out int kills))
{
kills = 0;
}
kills++;
weapons[weaponShortname] = kills;
SaveData();
return kills;
}
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PermAdmin, this);
LoadData();
}
private void Unload() => SaveData();
private void OnServerSave() => SaveData();
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
if (entity == null || info == null)
{
return;
}
if (!ShouldCountKill(entity))
{
return;
}
BasePlayer attacker = info.InitiatorPlayer;
if (attacker == null || attacker.IsNpc)
{
return;
}
BasePlayer victimPlayer = entity as BasePlayer;
if (victimPlayer != null && attacker == victimPlayer)
{
return;
}
ItemDefinition weaponDef = GetWeaponDefinition(info);
if (weaponDef == null || !ShouldTrackWeapon(weaponDef))
{
return;
}
int newKillCount = AddKill(attacker.userID, weaponDef.shortname);
if (newKillCount > 0 && newKillCount % _config.KillsPerTier == 0)
{
int tier = newKillCount / _config.KillsPerTier;
if (_config.MaxTiers > 0)
{
tier = Math.Min(tier, _config.MaxTiers);
}
HandleKillMilestone(attacker, weaponDef, newKillCount, tier);
}
}
private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (entity == null || info == null)
{
return;
}
BasePlayer attacker = info.InitiatorPlayer;
if (attacker == null)
{
return;
}
ItemDefinition weaponDef = GetWeaponDefinition(info);
if (weaponDef == null || !ShouldTrackWeapon(weaponDef))
{
return;
}
float multiplier = GetDamageMultiplier(attacker.userID, weaponDef.shortname);
if (multiplier <= 1f)
{
return;
}
info.damageTypes.ScaleAll(multiplier);
}
#endregion
#region Helpers
private ItemDefinition GetWeaponDefinition(HitInfo info)
{
Item weaponItem = info.Weapon?.GetItem();
if (weaponItem?.info != null)
{
return weaponItem.info;
}
AttackEntity attackEntity = info.Weapon;
if (attackEntity == null)
{
return null;
}
BasePlayer holder = attackEntity.GetOwnerPlayer();
Item heldItem = holder?.GetActiveItem();
return heldItem?.info;
}
private bool ShouldCountKill(BaseCombatEntity entity)
{
BasePlayer victim = entity as BasePlayer;
if (victim != null && !victim.IsNpc)
{
return true;
}
if (_config.TrackScientistKills && IsScientist(entity))
{
return true;
}
if (_config.PlayerKillsOnly)
{
return false;
}
return victim != null || IsScientist(entity);
}
private bool IsScientist(BaseCombatEntity entity)
{
if (entity == null)
{
return false;
}
if (entity is ScientistNPC)
{
return true;
}
string prefabName = entity.ShortPrefabName;
return !string.IsNullOrEmpty(prefabName)
&& prefabName.IndexOf("scientist", StringComparison.OrdinalIgnoreCase) >= 0;
}
private bool ShouldTrackWeapon(ItemDefinition weaponDef)
{
if (weaponDef == null)
{
return false;
}
if (_config.ExcludedWeapons != null && _config.ExcludedWeapons.Contains(weaponDef.shortname))
{
return false;
}
return weaponDef.category == ItemCategory.Weapon;
}
private float GetDamageMultiplier(ulong playerId, string weaponShortname)
{
int kills = GetKillCount(playerId, weaponShortname);
if (kills < _config.KillsPerTier)
{
return 1f;
}
int tiers = kills / _config.KillsPerTier;
if (_config.MaxTiers > 0)
{
tiers = Math.Min(tiers, _config.MaxTiers);
}
return 1f + (tiers * _config.DamageBonusPerTier);
}
private string FormatBonusLine(string weaponShortname, int kills)
{
int tiers = kills / _config.KillsPerTier;
float bonusPercent = tiers * _config.DamageBonusPerTier * 100f;
int killsToNext = tiers > 0
? _config.KillsPerTier - (kills % _config.KillsPerTier)
: _config.KillsPerTier - kills;
if (killsToNext == _config.KillsPerTier && kills > 0)
{
killsToNext = 0;
}
string nextTierText = killsToNext == 0
? "max tier reached for current kills"
: $"{killsToNext} kills to next +{_config.DamageBonusPerTier * 100f:0.#}%";
return $"{weaponShortname}: {kills} kills | +{bonusPercent:0.#}% damage | {nextTierText}";
}
private void HandleKillMilestone(BasePlayer player, ItemDefinition weaponDef, int killCount, int tier)
{
float bonusPercent = tier * _config.DamageBonusPerTier * 100f;
string weaponName = weaponDef.displayName.english;
if (_config.NotifyPlayerInChat)
{
player.ChatMessage(
$"Milestone reached: {killCount} kills with {weaponName}. Damage bonus is now +{bonusPercent:0.#}%.");
}
SendDiscordWebhook(player, weaponDef.shortname, weaponName, killCount, tier, bonusPercent);
}
private void SendDiscordWebhook(
BasePlayer player,
string weaponShortname,
string weaponName,
int killCount,
int tier,
float bonusPercent)
{
if (!_config.DiscordWebhookEnabled || string.IsNullOrWhiteSpace(_config.DiscordWebhookUrl))
{
return;
}
Dictionary<string, object> embed = new Dictionary<string, object>
{
["title"] = "Weapon Kill Milestone",
["color"] = _config.DiscordEmbedColor,
["fields"] = new List<Dictionary<string, object>>
{
new Dictionary<string, object>
{
["name"] = "Player",
["value"] = $"{player.displayName}\n`{player.UserIDString}`",
["inline"] = true
},
new Dictionary<string, object>
{
["name"] = "Weapon",
["value"] = $"{weaponName}\n`{weaponShortname}`",
["inline"] = true
},
new Dictionary<string, object>
{
["name"] = "Milestone",
["value"] = $"{killCount} kills\nTier {tier}",
["inline"] = true
},
new Dictionary<string, object>
{
["name"] = "New Damage Bonus",
["value"] = $"+{bonusPercent:0.#}%",
["inline"] = false
}
},
["footer"] = new Dictionary<string, object>
{
["text"] = $"Every {_config.KillsPerTier} kills = +{_config.DamageBonusPerTier * 100f:0.#}% damage"
},
["timestamp"] = DateTime.UtcNow.ToString("o")
};
Dictionary<string, object> payload = new Dictionary<string, object>
{
["username"] = _config.DiscordWebhookUsername,
["embeds"] = new List<Dictionary<string, object>> { embed }
};
string body = JsonConvert.SerializeObject(payload);
Dictionary<string, string> headers = new Dictionary<string, string>
{
["Content-Type"] = "application/json"
};
webrequest.Enqueue(
_config.DiscordWebhookUrl.Trim(),
body,
(code, response) =>
{
if (code != 200 && code != 204)
{
PrintWarning($"Discord webhook failed ({code}): {response}");
}
},
this,
RequestMethod.POST,
headers);
}
#endregion
#region Commands
[ChatCommand("weaponkills")]
private void CmdWeaponKills(BasePlayer player, string command, string[] args)
{
BasePlayer target = player;
if (args.Length > 0)
{
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage("You do not have permission to view another player's stats.");
return;
}
target = BasePlayer.FindAwakeOrSleeping(args[0]) ?? BasePlayer.FindByID(Convert.ToUInt64(args[0]));
if (target == null)
{
player.ChatMessage("Player not found.");
return;
}
}
if (!_data.PlayerWeaponKills.TryGetValue(target.userID, out Dictionary<string, int> weapons) || weapons.Count == 0)
{
player.ChatMessage(target == player
? "You have no tracked weapon kills yet."
: $"{target.displayName} has no tracked weapon kills yet.");
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine(target == player
? "Your weapon kill bonuses:"
: $"{target.displayName}'s weapon kill bonuses:");
foreach (KeyValuePair<string, int> entry in weapons.OrderByDescending(x => x.Value).Take(8))
{
sb.AppendLine(FormatBonusLine(entry.Key, entry.Value));
}
player.ChatMessage(sb.ToString().TrimEnd());
}
[ConsoleCommand("weaponkills.wipe")]
private void CmdWeaponKillsWipe(ConsoleSystem.Arg arg)
{
if (arg.Connection != null && !permission.UserHasPermission(arg.Connection.userid.ToString(), PermAdmin))
{
arg.ReplyWith("You do not have permission to use this command.");
return;
}
if (arg.Args == null || arg.Args.Length == 0)
{
arg.ReplyWith("Usage: weaponkills.wipe <steamid|all>");
return;
}
if (arg.Args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
_data.PlayerWeaponKills.Clear();
SaveData();
arg.ReplyWith("Cleared all weapon kill data.");
return;
}
if (!ulong.TryParse(arg.Args[0], out ulong userId))
{
arg.ReplyWith("Invalid SteamID.");
return;
}
_data.PlayerWeaponKills.Remove(userId);
SaveData();
arg.ReplyWith($"Cleared weapon kill data for {userId}.");
}
#endregion
}
}