C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
// Reference: System.Drawing
using Facepunch;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using Rust;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = System.Random;
using Time = UnityEngine.Time;
namespace Oxide.Plugins
{
[Info("HitMarker", "BlackWolf", "1.0.6")]
class HitMarker : RustPlugin
{
#region CONFIGURATION
private bool Changed;
private bool enablesound;
private string? soundeffect;
private string? headshotsoundeffect;
private float damageTimeout;
object GetConfig(string menu, string datavalue, object defaultValue)
{
var data = Config[menu] as Dictionary<string, object>;
if (data == null)
{
data = new Dictionary<string, object>();
Config[menu] = data;
Changed = true;
}
object value;
if (!data.TryGetValue(datavalue, out value))
{
value = defaultValue;
data[datavalue] = value;
Changed = true;
}
return value;
}
protected override void LoadDefaultConfig()
{
enablesound = Convert.ToBoolean(GetConfig("Sound", "EnableSoundEffect", true), System.Globalization.CultureInfo.InvariantCulture);
soundeffect = Convert.ToString(GetConfig("Sound", "Sound Effect", "assets/bundled/prefabs/fx/takedamage_hit.prefab"), System.Globalization.CultureInfo.InvariantCulture);
headshotsoundeffect = Convert.ToString(GetConfig("Sound", "HeadshotSoundEffect", "assets/bundled/prefabs/fx/headshot.prefab"), System.Globalization.CultureInfo.InvariantCulture);
GetVariable(Config, "Через сколько будет пропадать урон", out damageTimeout, 0.5f);
SaveConfig();
}
public static void GetVariable<T>( DynamicConfigFile config, string name, out T value, T defaultValue )
{
config[ name ] = value = config[ name ] == null ? defaultValue : (T) Convert.ChangeType( config[ name ], typeof( T ), System.Globalization.CultureInfo.InvariantCulture );
}
#endregion
#region FIELDS
[PluginReference] private Plugin? Clans;
Random rnd = new Random();
List<BasePlayer> hitmarkeron = new List<BasePlayer>();
Dictionary<BasePlayer, List<KeyValuePair<float, HitNfo>>> damageHistory = new Dictionary<BasePlayer, List<KeyValuePair<float, HitNfo>>>();
class HitNfo
{
public int damage;
public bool isHead;
public bool isFriend;
public float xs;
public float ys;
public float xe;
public float ye;
public int num;
public string entityType;
}
Dictionary<BasePlayer, Oxide.Plugins.Timer> destTimers = new Dictionary<BasePlayer, Oxide.Plugins.Timer>();
#endregion
#region COMMANDS
[ChatCommand("hitmarker")]
void cmdHitMarker(BasePlayer player, string cmd, string[] args)
{
if (!hitmarkeron.Contains(player))
{
hitmarkeron.Add(player);
SendReply(player,
"<color=#00ffff>HitMarker</color>:" + " " + "<color=orange>Вы включили показ урона.</color>");
}
else
{
hitmarkeron.Remove(player);
SendReply(player,
"<color=#00ffff>HitMarker</color>:" + " " + "<color=orange>Вы отключили показ урона.</color>");
}
}
#endregion
#region OXIDE HOOKS
void Unload()
{
// Очищаем списки игроков при выгрузке плагина
foreach (var player in BasePlayer.activePlayerList)
{
hitmarkeron.Remove(player);
damageHistory.Remove(player);
}
}
void OnServerInitialized(bool initial)
{
// Загружаем конфигурацию и добавляем всех активных игроков в список
LoadDefaultConfig();
foreach (BasePlayer current in BasePlayer.activePlayerList)
{
hitmarkeron.Add(current);
}
// Запускаем таймер для обновления индикаторов урона
timer.Every(0.1f, OnDamageTimer);
}
void OnPlayerConnected(BasePlayer player)
{
// Добавляем игрока в список при подключении
hitmarkeron.Add(player);
}
void OnPlayerDisconnected(BasePlayer player, string reason)
{
// Удаляем игрока из списков при отключении
hitmarkeron.Remove(player);
damageHistory.Remove(player);
}
void OnPlayerAttack(BasePlayer attacker, HitInfo hitinfo)
{
// Воспроизводим звуковые эффекты при атаке игрока
if (hitinfo?.HitEntity == null || attacker == null) return;
var victim = hitinfo.HitEntity as BasePlayer;
if (victim != null && hitmarkeron.Contains(attacker) && attacker.transform != null && attacker.net?.connection != null)
{
if (hitinfo.isHeadshot)
{
if (enablesound && headshotsoundeffect != null)
{
Effect.server.Run(headshotsoundeffect, attacker.transform.position, Vector3.zero,
attacker.net.connection);
}
}
else
{
if (enablesound && soundeffect != null)
{
Effect.server.Run(soundeffect, attacker.transform.position, Vector3.zero,
attacker.net.connection);
}
}
}
}
string ActionGUI = @"[
{
""name"": ""hitmarkerAction_{0}_{1}"",
""parent"": ""Overlay"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.4 0.2"",
""anchormax"": ""0.6 0.25""
}
]
},
{
""name"": ""hitmarkerActionText_{0}_{1}"",
""parent"": ""hitmarkerAction_{0}_{1}"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0 0"",
""anchormax"": ""1 1""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""{2}"",
""fontSize"": 16,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""{3}""
}
]
}
]";
string DamageGUI = @"[
{
""name"": ""hitmarkerDamage_{0}_{1}"",
""parent"": ""Overlay"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""{4} {5}"",
""anchormax"": ""{6} {7}""
}
]
},
{
""name"": ""hitmarkerDamageText_{0}_{1}"",
""parent"": ""hitmarkerDamage_{0}_{1}"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0 0"",
""anchormax"": ""1 1""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""{2}"",
""fontSize"": 14,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""{3}""
}
]
}
]";
static string HandleArgs(string json, params object[] args)
{
for (int i = 0; i < args.Length; i++)
{
var value = args[i];
string strValue;
if (value is float f)
{
strValue = f.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
else if (value is double d)
{
strValue = d.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
else if (value is IFormattable formattable)
{
strValue = formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture);
}
else
{
strValue = value?.ToString() ?? "";
}
json = json.Replace("{" + i + "}", strValue);
}
return json;
}
private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
{
// Initial checks
if (entity == null || hitInfo == null) return;
var attacker = hitInfo.InitiatorPlayer;
if (attacker == null || !hitmarkeron.Contains(attacker)) return;
var isHead = hitInfo.isHeadshot;
bool isFriend = false;
// Skip construction entities
if (entity is BuildingBlock || entity is Door || entity is StorageContainer ||
entity is BuildingPrivlidge || entity is BaseOven ||
entity is GunTrap || entity is BearTrap ||
entity is Barricade || entity is SimpleBuildingBlock) return;
// Check for friend
if (entity is BasePlayer targetPlayer)
{
isFriend = (Clans?.Call("HasFriend", attacker.userID, targetPlayer.userID) as bool?) ?? false;
}
// Store pre-hit values
float preHealth = entity.Health();
float maxHealth = entity.MaxHealth();
NextTick(() =>
{
// Get post-hit values
float postHealth = entity.Health();
// Calculate actual damage dealt (difference between pre and post health)
float actualDamage = Math.Max(0, preHealth - postHealth);
// Cap the displayed damage at the pre-hit health value
// This ensures we never show more damage than the entity had health
float cappedDamage = Math.Min(actualDamage, preHealth);
int damage = (int)Math.Round(cappedDamage);
// Skip if no real damage was dealt
if (damage <= 0) return;
// Calculate health values - use actual values, not percentages
int currentHealth = (int)Math.Round(postHealth);
int maxHealthInt = (int)Math.Round(maxHealth);
bool isDead = currentHealth <= 0 || entity.IsDead();
// Determine if this was a killing blow
bool isKillingBlow = isDead || currentHealth <= 0;
// Special handling for NPCs
if (entity is NPCPlayer)
{
bool isScientist = entity.ShortPrefabName?.Contains("scientist") == true;
if (isScientist && currentHealth > 0)
{
isKillingBlow = false;
}
}
// Check for corpse/harvestable
bool isCorpse = entity.ShortPrefabName?.Contains("corpse") ?? false;
bool isHarvestable = false;
if (isCorpse || isDead)
{
isHarvestable = hitInfo.damageTypes.Has(Rust.DamageType.Generic) ||
hitInfo.damageTypes.Has(Rust.DamageType.Blunt) ||
hitInfo.damageTypes.Has(Rust.DamageType.Slash);
if (!isHarvestable) return;
}
// Check for tool hits
bool isToolHit = hitInfo.WeaponPrefab != null &&
(hitInfo.damageTypes.Has(Rust.DamageType.Generic) ||
hitInfo.damageTypes.Has(Rust.DamageType.Blunt) ||
hitInfo.damageTypes.Has(Rust.DamageType.Slash));
// Determine entity type
string entityType = DetermineEntityType(entity);
// Show damage notification with capped damage value
if (damage > 0 || isToolHit)
{
DamageNotifier(attacker, damage, isHead, isFriend, entityType);
}
// Show appropriate hit indicator with actual health values
if (isDead || isKillingBlow)
{
ShowHitMarkerOnly(attacker, entityType, isHead, GetEntityDisplayName(entity));
}
else
{
ShowHitIndicator(attacker, entityType, isHead, GetEntityDisplayName(entity), currentHealth, maxHealthInt);
}
});
}
// Helper method to determine entity type
private string DetermineEntityType(BaseCombatEntity entity)
{
if (entity is BasePlayer) return "player";
if (entity is NPCPlayer) return "npc";
if (IsAnimal(entity)) return "animal";
if (entity.ShortPrefabName?.Contains("barrel") == true ||
entity.ShortPrefabName?.Contains("loot-barrel") == true ||
entity.ShortPrefabName?.Contains("loot_barrel") == true) return "barrel";
if (entity is BradleyAPC ||
entity is BaseHelicopter ||
entity is BaseVehicle ||
entity.ShortPrefabName.Contains("minicopter") ||
entity.ShortPrefabName.Contains("scraptransport") ||
entity.ShortPrefabName.Contains("rowboat") ||
entity.ShortPrefabName.Contains("rhib") ||
entity.ShortPrefabName.Contains("hotairballoon") ||
entity.ShortPrefabName.Contains("submarine") ||
entity.ShortPrefabName.Contains("modularcar") ||
entity.ShortPrefabName.Contains("snowmobile") ||
entity.ShortPrefabName.Contains("workcart") ||
entity.ShortPrefabName.Contains("tugboat") ||
entity.ShortPrefabName.Contains("sedan") ||
entity.ShortPrefabName.Contains("tank")) return "vehicle";
return "other";
}
// Helper method to get entity display name
private string GetEntityDisplayName(BaseCombatEntity entity)
{
if (entity is BasePlayer) return "Player";
if (entity is NPCPlayer) return "Scientist";
if (IsAnimal(entity)) return GetAnimalDisplayName(entity);
if (entity.ShortPrefabName?.Contains("barrel") == true) return GetBarrelDisplayName(entity);
if (entity is BradleyAPC || entity is BaseHelicopter || entity is BaseVehicle) return GetVehicleDisplayName(entity);
if (entity is AutoTurret) return "Auto Turret";
if (entity is SamSite) return "SAM Site";
return entity.ShortPrefabName ?? "Unknown";
}
// Helper method to check if an entity is an animal
private bool IsAnimal(BaseCombatEntity entity)
{
if (entity is BaseAnimalNPC) return true;
if (entity is RidableHorse2) return true;
if (entity.ShortPrefabName?.Contains("horse") == true) return true;
// Check for specific prefab names for wolves and other animals
string prefabName = entity.ShortPrefabName?.ToLower() ?? "";
return prefabName.Contains("wolf") ||
prefabName.Contains("bear") ||
prefabName.Contains("boar") ||
prefabName.Contains("stag") ||
prefabName.Contains("chicken") ||
prefabName.Contains("horse") ||
prefabName.Contains("deer") ||
prefabName.Contains("polarbear");
}
// Helper method to get a proper display name for animals
private string GetAnimalDisplayName(BaseCombatEntity entity)
{
if (entity is RidableHorse2) return "Horse";
string prefabName = entity.ShortPrefabName?.ToLower() ?? "";
if (prefabName.Contains("wolf")) return "Wolf";
if (prefabName.Contains("bear") && prefabName.Contains("polar")) return "Polar Bear";
if (prefabName.Contains("bear")) return "Bear";
if (prefabName.Contains("boar")) return "Boar";
if (prefabName.Contains("stag") || prefabName.Contains("deer")) return "Deer";
if (prefabName.Contains("chicken")) return "Chicken";
if (prefabName.Contains("horse")) return "Horse";
// Default to the short prefab name if we can't identify it
return entity.ShortPrefabName;
}
// Helper method to get a proper display name for vehicles
private string GetVehicleDisplayName(BaseCombatEntity entity)
{
string prefabName = entity.ShortPrefabName?.ToLower() ?? "";
if (prefabName.Contains("bradley")) return "Bradley";
if (prefabName.Contains("patrol")) return "Patrol Heli";
if (prefabName.Contains("minicopter")) return "Mini";
if (prefabName.Contains("scraptransport")) return "Scrap Heli";
if (prefabName.Contains("rowboat")) return "Boat";
if (prefabName.Contains("rhib")) return "RHIB";
if (prefabName.Contains("hotairballoon")) return "Balloon";
if (prefabName.Contains("submarine")) return "Sub";
if (prefabName.Contains("modularcar")) return "Car";
if (prefabName.Contains("snowmobile")) return "Snowmobile";
if (prefabName.Contains("workcart")) return "Train";
if (prefabName.Contains("tugboat")) return "Tugboat";
if (prefabName.Contains("sedan")) return "Sedan";
if (prefabName.Contains("tank")) return "Tank";
// Default to a generic "Vehicle" if we can't identify it
return "Vehicle";
}
// Helper method to get a proper display name for barrels
private string GetBarrelDisplayName(BaseCombatEntity entity)
{
string prefabName = entity.ShortPrefabName?.ToLower() ?? "";
if (prefabName.Contains("oil")) return "Oil Barrel";
if (prefabName.Contains("fuel")) return "Fuel Barrel";
if (prefabName.Contains("water")) return "Water Barrel";
if (prefabName.Contains("blue")) return "Blue Barrel";
if (prefabName.Contains("red")) return "Red Barrel";
if (prefabName.Contains("yellow")) return "Yellow Barrel";
if (prefabName.Contains("white")) return "White Barrel";
if (prefabName.Contains("green")) return "Green Barrel";
if (prefabName.Contains("black")) return "Black Barrel";
if (prefabName.Contains("grey") || prefabName.Contains("gray")) return "Grey Barrel";
if (prefabName.Contains("loot")) return "Loot Barrel";
// Default to a generic "Barrel" if we can't identify it
return "Barrel";
}
private string GetTargetColor(string targetType, bool isHeadshot, string entityName = "")
{
return targetType switch
{
"player" => isHeadshot ? "0.8 0.1 0.1 1" : "1 0.3 0.3 1", // Темно-красный для хедшотов, светло-красный для обычного урона
"npc" => isHeadshot ? "0.8 0.1 0.1 1" : "0.6 0.3 0.1 1", // Темно-красный для хедшотов, коричневый для NPC
"animal" => isHeadshot ? "0.8 0.1 0.1 1" : "0 0.6 0 1", // Темно-красный для хедшотов, темно-зеленый для животных
"barrel" => "0.9 0.6 0.1 1", // Оранжевый для бочек
"vehicle" => isHeadshot ? "0.8 0.1 0.1 1" : entityName.ToLower() switch
{
"bradley" => "1 0.4 0 1", // Оранжевый для Bradley
"patrol heli" => "1 0 0 1", // Красный для патрульного вертолета
"mini" => "0.2 0.6 1 1", // Голубой для вертолетов
"scrap heli" => "0.2 0.6 1 1", // Голубой для вертолетов
"balloon" => "1 0.6 0.8 1", // Розовый для воздушных шаров
"boat" => "0 0.8 1 1", // Синий для лодок
"rhib" => "0 0.8 1 1", // Синий для лодок
"car" => "0.5 0.5 0.5 1", // Темно-серый для машин
"sedan" => "0.5 0.5 0.5 1", // Темно-серый для машин
"tank" => "0.4 0.4 0.4 1", // Еще более темно-серый для танков
_ => "0.6 0.6 0.6 1" // Серый по умолчанию для транспорта
},
_ => "1 1 1 1" // Белый по умолчанию
};
}
private string GetHealthBar(float currentHealth, float maxHealth, string entityType)
{
// Ensure health values are valid
currentHealth = Math.Max(0, currentHealth);
maxHealth = Math.Max(1, maxHealth);
// Calculate percentage for color and bar fill only
float healthPercent = Math.Min(1, currentHealth / maxHealth);
string healthColor = healthPercent switch
{
var h when h > 0.7f => "#00ff00",
var h when h > 0.3f => "#ffa500",
_ => "#ff0000"
};
// Create health bar segments
int filledSegments = (int)(healthPercent * 15);
int emptySegments = 15 - filledSegments;
string filledBar = new string('█', filledSegments);
string emptyBar = new string('░', emptySegments);
string healthBarHtml = $@"<size=14>
[<color={healthColor}>{filledBar}</color>{emptyBar}]</size>";
// Show only current HP value, rounded to nearest integer
return $"{healthBarHtml}\n<color={healthColor}>{(int)Math.Round(currentHealth)}</color>";
}
void DamageNotifier(BasePlayer player, int damage, bool isHead, bool isFriend, string entityType = "player")
{
if (player == null) return;
// Don't show damage notification if damage is 0 or negative
if (damage <= 0) return;
List<KeyValuePair<float, HitNfo>> damages;
if (!damageHistory.TryGetValue(player, out damages))
damageHistory[player] = damages = new List<KeyValuePair<float, HitNfo>>();
// Limit the number of simultaneous damage notifications to prevent overload
if (damages.Count >= 5)
{
// Remove the oldest notification if we have too many
if (damages.Count > 0)
{
var oldest = damages[damages.Count - 1];
string oldNum = oldest.Value.num.ToString(System.Globalization.CultureInfo.InvariantCulture);
CuiHelper.DestroyUi(player, $"hitmarkerDamageText_{oldNum}");
CuiHelper.DestroyUi(player, $"hitmarkerDamage_{oldNum}");
damages.RemoveAt(damages.Count - 1);
}
}
// Определяем тип цели для правильного цвета
string targetType = isFriend ? "friend" : entityType;
// Generate random position around crosshair
float randomX = 0.45f + (float)(rnd.NextDouble() - 0.5) * 0.1f;
float randomY = 0.4f + (float)(rnd.NextDouble() - 0.5) * 0.1f;
// Get color based on entity type and health percentage
string damageColor;
if (isHead)
{
damageColor = "#800000"; // Темно-красный для хедшотов
}
else if (isFriend)
{
damageColor = "#00ff00"; // Зеленый для друзей
}
else
{
// Match the health bar color scheme
damageColor = entityType switch
{
"player" => "#ff4d4d", // Светло-красный для игроков
"npc" => "#964B00", // Коричневый для NPCs
"animal" => "#006400", // Темно-зеленый для животных
"vehicle" => "#808080",// Серый для транспорта
_ => "#ffffff" // Белый по умолчанию
};
}
var damageText = isHead ?
$"<color={damageColor}>⊕ -{damage}</color>" :
$"<color={damageColor}>-{damage}</color>";
int timestamp = (int)Time.realtimeSinceStartup;
string timestampStr = timestamp.ToString(System.Globalization.CultureInfo.InvariantCulture);
int uniqueNum = rnd.Next(0, 10000);
string id = uniqueNum.ToString(System.Globalization.CultureInfo.InvariantCulture);
string uniqueId = $"{timestampStr}_{id}";
// Очищаем старые индикаторы перед показом новых (но не все, только если их слишком много)
if (damages.Count > 10)
{
foreach (var oldDamage in damages.Skip(5))
{
string oldNum = oldDamage.Value.num.ToString(System.Globalization.CultureInfo.InvariantCulture);
CuiHelper.DestroyUi(player, $"hitmarkerDamageText_{oldNum}");
CuiHelper.DestroyUi(player, $"hitmarkerDamage_{oldNum}");
}
// Keep only the 5 newest notifications
damages = damages.Take(5).ToList();
damageHistory[player] = damages;
}
damages.Insert(0, new KeyValuePair<float, HitNfo>(Time.time + damageTimeout, new HitNfo
{
damage = damage,
isHead = isHead,
isFriend = isFriend,
num = uniqueNum,
xs = randomX,
ys = randomY,
xe = randomX + 0.1f,
ye = randomY + 0.05f,
entityType = entityType
}));
// Updated DamageGUI to use larger font size for better visibility
string updatedDamageGUI = @"[
{
""name"": ""hitmarkerDamage_{0}_{1}"",
""parent"": ""Overlay"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""{4} {5}"",
""anchormax"": ""{6} {7}""
}
]
},
{
""name"": ""hitmarkerDamageText_{0}_{1}"",
""parent"": ""hitmarkerDamage_{0}_{1}"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0 0"",
""anchormax"": ""1 1""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""{2}"",
""fontSize"": 20,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""1 1 1 1""
}
]
}
]";
CuiHelper.AddUi(player, HandleArgs(updatedDamageGUI, timestampStr, id, damageText, "1 1 1 1",
randomX.ToString(System.Globalization.CultureInfo.InvariantCulture),
randomY.ToString(System.Globalization.CultureInfo.InvariantCulture),
(randomX + 0.1f).ToString(System.Globalization.CultureInfo.InvariantCulture),
(randomY + 0.05f).ToString(System.Globalization.CultureInfo.InvariantCulture)));
destTimers[player] = timer.Once(damageTimeout, () =>
{
CuiHelper.DestroyUi(player, $"hitmarkerDamageText_{uniqueId}");
CuiHelper.DestroyUi(player, $"hitmarkerDamage_{uniqueId}");
// Use the actual num value for removal instead of generating a new random number
damages.RemoveAll(x => x.Value.num == uniqueNum);
});
}
// New method to show only hit marker without health bar
private void ShowHitMarkerOnly(BasePlayer player, string targetType, bool isHeadshot, string entityName = "")
{
if (player == null) return;
string textColor = GetTargetColor(targetType, isHeadshot, entityName);
// Clear existing UI elements
CuiHelper.DestroyUi(player, "hitmarkerActionText_hit");
CuiHelper.DestroyUi(player, "hitmarkerAction_hit");
CuiHelper.DestroyUi(player, "hitmarkerHealth");
CuiHelper.DestroyUi(player, "hitmarkerHealthText");
CuiHelper.DestroyUi(player, "hitmarkerHealthBar");
CuiHelper.DestroyUi(player, "hitmarkerHealthFill");
CuiHelper.DestroyUi(player, "hitmarkerHealthIcon");
CuiHelper.DestroyUi(player, "hitmarkerHealthBorder");
CuiHelper.DestroyUi(player, "hitmarkerHealthSegments");
CuiHelper.DestroyUi(player, "hitmarkerHealthPercent");
// Format hit text with emoji for headshot
var hitText = targetType switch
{
"player" => isHeadshot ? "⊕ HIT!" : "HIT!",
"npc" => isHeadshot ? "⊕ HIT!" : "HIT!",
"animal" => isHeadshot ? $"⊕ {entityName.ToUpper()} HIT!" : $"{entityName.ToUpper()} HIT!",
"vehicle" => $"{entityName.ToUpper()} HIT!",
_ => "HIT!"
};
// Updated hit marker GUI with better styling
var hitMarkerGui = @"[
{
""name"": ""hitmarkerAction_hit"",
""parent"": ""Overlay"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.4 0.2"",
""anchormax"": ""0.6 0.25""
}
]
},
{
""name"": ""hitmarkerActionText_hit"",
""parent"": ""hitmarkerAction_hit"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0 0"",
""anchormax"": ""1 1""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""{0}"",
""fontSize"": 18,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""{1}""
}
]
}
]";
CuiHelper.AddUi(player, HandleArgs(hitMarkerGui, hitText, textColor));
// Updated killed message UI with rounded corners and better styling
string killedBarGui = @"[
{
""name"": ""hitmarkerHealth"",
""parent"": ""Overlay"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.4 0.15"",
""anchormax"": ""0.6 0.2""
}
]
},
{
""name"": ""hitmarkerHealthBorder"",
""parent"": ""hitmarkerHealth"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.05 0.2"",
""anchormax"": ""0.95 0.8""
},
{
""type"": ""UnityEngine.UI.Image"",
""color"": ""0.12 0.12 0.12 0.95"",
""material"": ""assets/content/ui/ui.background.transparent.radial.psd"",
""sprite"": ""assets/content/ui/ui.background.transparent.radial.psd""
}
]
},
{
""name"": ""hitmarkerHealthBar"",
""parent"": ""hitmarkerHealthBorder"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.01 0.1"",
""anchormax"": ""0.99 0.9""
},
{
""type"": ""UnityEngine.UI.Image"",
""color"": ""0.08 0.08 0.08 0.9"",
""material"": ""assets/content/ui/ui.background.transparent.radial.psd"",
""sprite"": ""assets/content/ui/ui.background.transparent.radial.psd""
}
]
},
{
""name"": ""hitmarkerHealthIcon"",
""parent"": ""hitmarkerHealth"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0 0.2"",
""anchormax"": ""0.1 0.8""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""☠"",
""fontSize"": 22,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""1 0 0 1""
}
]
},
{
""name"": ""hitmarkerHealthPercent"",
""parent"": ""hitmarkerHealth"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.1 0"",
""anchormax"": ""0.9 1""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""KILLED"",
""fontSize"": 16,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""1 0 0 1""
}
]
}
]";
CuiHelper.AddUi(player, killedBarGui);
// Set synchronized timers for UI removal
timer.Once(damageTimeout, () =>
{
CuiHelper.DestroyUi(player, "hitmarkerActionText_hit");
CuiHelper.DestroyUi(player, "hitmarkerAction_hit");
CuiHelper.DestroyUi(player, "hitmarkerHealth");
CuiHelper.DestroyUi(player, "hitmarkerHealthIcon");
CuiHelper.DestroyUi(player, "hitmarkerHealthBorder");
CuiHelper.DestroyUi(player, "hitmarkerHealthBar");
CuiHelper.DestroyUi(player, "hitmarkerHealthPercent");
});
}
private string GetHealthBarColor(float healthPercent)
{
if (healthPercent > 0.7f) return "0 0.8 0 0.9"; // Green
if (healthPercent > 0.3f) return "1 0.65 0 0.9"; // Orange
return "1 0 0 0.9"; // Red
}
private void ShowHitIndicator(BasePlayer player, string targetType, bool isHeadshot, string entityName, int health, int maxHealth)
{
if (player == null) return;
string textColor = GetTargetColor(targetType, isHeadshot, entityName);
// Clear existing UI elements
CuiHelper.DestroyUi(player, "hitmarkerActionText_hit");
CuiHelper.DestroyUi(player, "hitmarkerAction_hit");
CuiHelper.DestroyUi(player, "hitmarkerHealth");
CuiHelper.DestroyUi(player, "hitmarkerHealthText");
CuiHelper.DestroyUi(player, "hitmarkerHealthBar");
CuiHelper.DestroyUi(player, "hitmarkerHealthFill");
CuiHelper.DestroyUi(player, "hitmarkerHealthIcon");
CuiHelper.DestroyUi(player, "hitmarkerHealthBorder");
CuiHelper.DestroyUi(player, "hitmarkerHealthSegments");
CuiHelper.DestroyUi(player, "hitmarkerHealthPercent");
// Format hit text
var hitText = targetType switch
{
"player" => isHeadshot ? "⊕ HIT!" : "HIT!",
"npc" => isHeadshot ? "⊕ HIT!" : "HIT!",
"animal" => isHeadshot ? $"⊕ {entityName.ToUpper()} HIT!" : $"{entityName.ToUpper()} HIT!",
"vehicle" => $"{entityName.ToUpper()} HIT!",
_ => "HIT!"
};
// Updated hit marker GUI with better styling
var hitMarkerGui = @"[
{
""name"": ""hitmarkerAction_hit"",
""parent"": ""Overlay"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.4 0.2"",
""anchormax"": ""0.6 0.25""
}
]
},
{
""name"": ""hitmarkerActionText_hit"",
""parent"": ""hitmarkerAction_hit"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0 0"",
""anchormax"": ""1 1""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""{0}"",
""fontSize"": 18,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""{1}""
}
]
}
]";
CuiHelper.AddUi(player, HandleArgs(hitMarkerGui, hitText, textColor));
// Show health bar if entity has health
if (health > 0)
{
float healthPercent = Math.Min(1, (float)health / maxHealth);
string healthColor = GetHealthBarColor(healthPercent);
// Updated health bar GUI with rounded corners and better styling
string healthBarGui = @"[
{
""name"": ""hitmarkerHealth"",
""parent"": ""Overlay"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.4 0.15"",
""anchormax"": ""0.6 0.2""
}
]
},
{
""name"": ""hitmarkerHealthBorder"",
""parent"": ""hitmarkerHealth"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.05 0.2"",
""anchormax"": ""0.95 0.8""
},
{
""type"": ""UnityEngine.UI.Image"",
""color"": ""0.12 0.12 0.12 0.95"",
""material"": ""assets/content/ui/ui.background.transparent.radial.psd"",
""sprite"": ""assets/content/ui/ui.background.transparent.radial.psd""
}
]
},
{
""name"": ""hitmarkerHealthBar"",
""parent"": ""hitmarkerHealthBorder"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.01 0.1"",
""anchormax"": ""0.99 0.9""
},
{
""type"": ""UnityEngine.UI.Image"",
""color"": ""0.08 0.08 0.08 0.9"",
""material"": ""assets/content/ui/ui.background.transparent.radial.psd"",
""sprite"": ""assets/content/ui/ui.background.transparent.radial.psd""
}
]
},
{
""name"": ""hitmarkerHealthFill"",
""parent"": ""hitmarkerHealthBar"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0 0"",
""anchormax"": ""{0} 1""
},
{
""type"": ""UnityEngine.UI.Image"",
""color"": ""{1}"",
""material"": ""assets/content/ui/ui.background.transparent.radial.psd"",
""sprite"": ""assets/content/ui/ui.background.transparent.radial.psd""
}
]
},
{
""name"": ""hitmarkerHealthText"",
""parent"": ""hitmarkerHealth"",
""components"": [
{
""type"": ""RectTransform"",
""anchormin"": ""0.1 0"",
""anchormax"": ""0.9 1""
},
{
""type"": ""UnityEngine.UI.Text"",
""text"": ""{2} HP"",
""fontSize"": 16,
""font"": ""robotocondensed-bold.ttf"",
""align"": ""MiddleCenter"",
""color"": ""{1}""
}
]
}
]";
CuiHelper.AddUi(player, HandleArgs(healthBarGui,
healthPercent.ToString(System.Globalization.CultureInfo.InvariantCulture),
healthColor,
health.ToString(System.Globalization.CultureInfo.InvariantCulture)));
// Set synchronized timers for UI removal
timer.Once(5f, () =>
{
CuiHelper.DestroyUi(player, "hitmarkerHealth");
CuiHelper.DestroyUi(player, "hitmarkerHealthText");
CuiHelper.DestroyUi(player, "hitmarkerHealthBar");
CuiHelper.DestroyUi(player, "hitmarkerHealthFill");
CuiHelper.DestroyUi(player, "hitmarkerHealthBorder");
});
}
// Set timer to remove hit marker
timer.Once(damageTimeout, () =>
{
CuiHelper.DestroyUi(player, "hitmarkerActionText_hit");
CuiHelper.DestroyUi(player, "hitmarkerAction_hit");
});
}
void OnDamageTimer()
{
var toRemove = Pool.Get<List<BasePlayer>>();
// Process only a limited number of players per tick to reduce server load
int processedPlayers = 0;
int maxPlayersPerTick = 5; // Limit the number of players processed per tick
foreach (var dmgHistoryKVP in damageHistory)
{
// Skip if we've processed enough players this tick
if (processedPlayers >= maxPlayersPerTick) break;
DrawDamageNotifier(dmgHistoryKVP.Key);
if (dmgHistoryKVP.Value.Count == 0)
toRemove.Add(dmgHistoryKVP.Key);
processedPlayers++;
}
toRemove.ForEach(p=>damageHistory.Remove(p));
Pool.FreeList(ref toRemove);
}
void DestroyLastCui(BasePlayer player)
{
Oxide.Plugins.Timer tmr;
if (destTimers.TryGetValue(player, out tmr))
{
tmr?.Callback?.Invoke();
if (tmr != null && !tmr.Destroyed)
timer.Destroy(ref tmr);
}
}
void OnEntityDeath(BaseCombatEntity entity, HitInfo hitInfo)
{
if (entity == null || hitInfo == null) return;
var attacker = hitInfo.InitiatorPlayer;
if (attacker == null || !hitmarkeron.Contains(attacker)) return;
// Skip construction entities
if (entity is BuildingBlock || entity is Door || entity is StorageContainer ||
entity is BuildingPrivlidge || entity is BaseOven ||
entity is GunTrap || entity is BearTrap ||
entity is Barricade || entity is SimpleBuildingBlock) return;
var isHead = hitInfo.isHeadshot;
bool isFriend = false;
// Check if target is a player and if they're a friend
if (entity is BasePlayer targetPlayer)
{
isFriend = (Clans?.Call("HasFriend", attacker.userID, targetPlayer.userID) as bool?) ?? false;
}
// Determine entity type
string entityType = DetermineEntityType(entity);
// Get damage amount, ensure it's at least 1 for death notifications
int damage = (int)hitInfo.damageTypes.Total();
if (damage <= 0) damage = 1;
// Check if this is a splash damage kill (multiple entities killed at once)
bool isSplashDamage = hitInfo.damageTypes.Has(Rust.DamageType.Explosion) ||
hitInfo.damageTypes.Has(Rust.DamageType.Heat) ||
hitInfo.damageTypes.Has(Rust.DamageType.ElectricShock);
// For splash damage, only show notifications for important entities
if (isSplashDamage)
{
// Skip less important entities during splash damage to reduce load
if (entityType == "animal" && !(entity is RidableHorse2)) return;
if (entityType == "npc" && !entity.ShortPrefabName.Contains("scientist")) return;
// For remaining entities, use a simpler notification
DamageNotifier(attacker, damage, isHead, isFriend, entityType);
// Only show health bar for players and important entities
if (entityType == "player" || entity is RidableHorse2 || entity.ShortPrefabName.Contains("scientist"))
{
if (entityType == "player")
ShowHitMarkerOnly(attacker, "player", isHead);
else if (entityType == "npc")
ShowHitMarkerOnly(attacker, "npc", isHead);
else if (entityType == "animal")
{
string entityName = GetAnimalDisplayName(entity);
ShowHitMarkerOnly(attacker, "animal", isHead, entityName);
}
}
return;
}
// For non-splash damage kills, show full notifications
// Show appropriate death notification based on entity type
if (entity is BasePlayer)
{
// Show death notification
DamageNotifier(attacker, damage, isHead, isFriend, entityType);
ShowHitMarkerOnly(attacker, "player", isHead);
}
else if (entity is NPCPlayer)
{
// Special handling for scientists
bool isScientist = entity.ShortPrefabName?.Contains("scientist") == true ||
entity.ShortPrefabName?.Contains("npc") == true;
// Ensure we always show KILLED for scientists
if (isScientist)
{
// Show death notification with guaranteed damage
// ... [Code preview truncated due to length. Full source in attached file] ...