Merge pull request #57 from ByDexterTR/main

v1.2.4.b1
This commit is contained in:
Juzlus 2026-09-08 01:56:11 +02:00 • committed by GitHub
commit 4012bf8f6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 730 additions and 227 deletions

View file

@ -31,7 +31,7 @@ namespace src
public override string ModuleName => "[CS2] [ jRandomSkills ]";
public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)";
public override string ModuleDescription => "Plugin adds random skills every round for CS2 by D3X. Modified by Juzlus.";
public override string ModuleVersion => "1.2.3.b9";
public override string ModuleVersion => "1.2.4.b1";
public override void Load(bool hotReload)
{

View file

@ -82,7 +82,7 @@
"catapult": "Mancınık",
"catapult_desc": "Vurduğun rakibi uzaya (yukarı doğru) fırlatma şansın var",
"catapult_desc2": "Fırlatma şansın: %{0}",
"catapult_desc2": "Vurduğun rakibi havaya fırlatma şansın: %{0}",
"chameleon": "Bukalemun",
"chameleon_desc": "Öldürdüğün ilk oyuncunun yeteneği sana geçer",
@ -452,7 +452,7 @@
"push": "İtici",
"push_desc": "Düşmana vurduğunda onu geri itme şansın olur",
"push_desc2": "Geri itme şansın: %{0}",
"push_desc2": "Vurduğun rakibi geri itme şansın: %{0}",
"pyro": "Ateşbaz",
"pyro_desc": "Molotof canını yeniler",
@ -511,7 +511,7 @@
"shade": "Gölge",
"shade_desc": "Vurduğun düşmanın arkasına ışınlanma şansın olur",
"shade_desc2": "Arkasına ışınlanma şansın: %{0}",
"shade_desc2": "Vurduğun rakibin arkasına ışınlanma şansın: %{0}",
"shade_nospace": "Uygun alan yok",
"shortbomb": "Kısa Fünye",

View file

@ -294,10 +294,14 @@ namespace src.player
}
}
private static bool IsEventAlive(GameEvent? @event) => @event != null && @event.Handle != nint.Zero;
private static HookResult WeaponEquip(EventItemEquip @event, GameEventInfo info)
{
lock (setLock)
{
if (!IsEventAlive(@event)) return HookResult.Continue;
DispatchToActiveSkills("WeaponEquip", @event);
return HookResult.Continue;
}
@ -307,6 +311,8 @@ namespace src.player
{
lock (setLock)
{
if (!IsEventAlive(@event)) return HookResult.Continue;
DispatchToActiveSkills("WeaponPickup", @event);
return HookResult.Continue;
}
@ -584,7 +590,7 @@ namespace src.player
string welcomeMsg = player.GetTranslationWithoutIlliterate("welcome_message", "welcome");
foreach (string line in welcomeMsg.Split("\n"))
player.PrintToChat($" {ChatColors.Green}" + line.Replace("{PLAYER}", $" {ChatColors.Red}\u202A{player.PlayerName}\u202C{ChatColors.Green}", StringComparison.OrdinalIgnoreCase)
.Replace("{SERVER_NAME}", $" {ChatColors.Red}{ConVar.Find("hostname")?.StringValue ?? "Default Server"}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase)
.Replace("{SERVER_NAME}", $" {ChatColors.Red}{SkillUtils.CvarString("hostname", "Default Server")}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase)
.Replace("{VERSION}", $" {ChatColors.Red}v{Instance.ModuleVersion}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase)
.Replace("{SKILLS_COUNT}", $" {ChatColors.Red}{SkillData.Skills.Count - 1}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase)
.Replace("{AUTHOR1}", $" {ChatColors.Red}Jakub Bartosik (D3X){ChatColors.Green} ({ChatColors.Red}https://github.com/jakubbartosik/dRandomSkills{ChatColors.Green})", StringComparison.OrdinalIgnoreCase)
@ -776,6 +782,7 @@ namespace src.player
if (pawn.AbsOrigin == null || pawn.AbsRotation == null) return;
if (pawn.IsDefusing) return;
if (IsAimingAtPlantedBomb(player, pawn)) return;
Vector eyePos = new(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + pawn.ViewOffset.Z);
Vector endPos = eyePos + SkillUtils.GetForwardVector(pawn.EyeAngles) * 80;
@ -809,6 +816,35 @@ namespace src.player
}
}
private const float BombBlockRange = 100f;
private const float BombBlockCos = 0.7071f;
private static bool IsAimingAtPlantedBomb(CCSPlayerController player, CCSPlayerPawn pawn)
{
if (player.Team != CsTeam.CounterTerrorist) return false;
if (pawn.AbsOrigin == null) return false;
foreach (var bomb in Utilities.FindAllEntitiesByDesignerName<CPlantedC4>("planted_c4"))
{
if (bomb == null || !bomb.IsValid || bomb.AbsOrigin == null) continue;
if (!bomb.BombTicking || bomb.BombDefused) continue;
float dx = bomb.AbsOrigin.X - pawn.AbsOrigin.X;
float dy = bomb.AbsOrigin.Y - pawn.AbsOrigin.Y;
float dz = bomb.AbsOrigin.Z - (pawn.AbsOrigin.Z + pawn.ViewOffset.Z);
float distance = MathF.Sqrt(dx * dx + dy * dy + dz * dz);
if (distance > BombBlockRange || distance <= 0.01f) continue;
var forward = SkillUtils.GetForwardVector(pawn.EyeAngles);
float dot = (forward.X * dx + forward.Y * dy + forward.Z * dz) / distance;
if (dot >= BombBlockCos) return true;
}
return false;
}
private static HookResult BulletImpact(EventBulletImpact @event, GameEventInfo info)
{
lock (setLock)

View file

@ -2,7 +2,6 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Modules.Admin;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
@ -20,7 +19,17 @@ namespace src.player
{
public static partial class Event
{
private static jSkill_SkillInfo ChooseSkillByRarityAndMax(List<jSkill_SkillInfo> candidates, Dictionary<Skills, int> assignmentCounts, Config.GameModes gameMode)
private static bool IsVip(CCSPlayerController? player)
{
if (player == null || !player.IsValid || player.IsBot) return false;
string flag = Config.LoadedConfig.VIPFlag;
if (string.IsNullOrWhiteSpace(flag)) return false;
return AdminManager.PlayerHasPermissions(player, flag);
}
private static jSkill_SkillInfo ChooseSkillByRarityAndMax(List<jSkill_SkillInfo> candidates, Dictionary<Skills, int> assignmentCounts, Config.GameModes gameMode, bool vip)
{
if (candidates == null || candidates.Count == 0) return noneSkill;
@ -31,7 +40,7 @@ namespace src.player
for (int attempt = 0; attempt < attempts; attempt++)
{
var (roll, rolled) = RarityManager.RollRarity();
var (roll, rolled) = RarityManager.RollRarity(vip);
string rolledName = rolled.ToString();
filtered.Clear();
@ -92,7 +101,7 @@ namespace src.player
}
Instance.RemoveListener<CheckTransmit>(CheckTransmit);
int freezetime = ConVar.Find("mp_freezetime")?.GetPrimitiveValue<Int32>() ?? 0;
int freezetime = SkillUtils.CvarValue("mp_freezetime", 0);
freezeTimeEnd = DateTime.Now.AddSeconds(freezetime + (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0));
setSkillTimer?.Kill();
@ -212,7 +221,7 @@ namespace src.player
PlayerManager.Clear();
ConVar.Find("sv_legacy_jump")?.SetValue("1");
SkillUtils.Cvar("sv_legacy_jump")?.SetValue("1");
}
}
@ -283,6 +292,7 @@ namespace src.player
public required HashSet<Skills> CtOnly { get; init; }
public required HashSet<Skills> TOnly { get; init; }
public required HashSet<Skills> PistolRoundBanned { get; init; }
public required HashSet<Skills> MinPlayerBanned { get; init; }
public required int TerroristCount { get; init; }
public required int CounterTerroristCount { get; init; }
}
@ -307,6 +317,7 @@ namespace src.player
PistolRoundBanned = SkillUtils.IsPistolRound()
? ToSkillSet(SkillsInfo.LoadedConfig.Where(s => s.DisableOnPistolRound).Select(s => s.Name))
: [],
MinPlayerBanned = ToSkillSet(SkillsInfo.LoadedConfig.Where(s => s.MinPlayer > 0 && validPlayers.Count < s.MinPlayer).Select(s => s.Name)),
TerroristCount = validPlayers.Count(p => p.Team == CsTeam.Terrorist),
CounterTerroristCount = validPlayers.Count(p => p.Team == CsTeam.CounterTerrorist),
};
@ -342,13 +353,16 @@ namespace src.player
if (ctx.PistolRoundBanned.Count != 0)
skillList.RemoveAll(s => ctx.PistolRoundBanned.Contains(s.Skill));
if (ctx.MinPlayerBanned.Count != 0)
skillList.RemoveAll(s => ctx.MinPlayerBanned.Contains(s.Skill));
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out HashSet<Skills>? skills))
{
skillList.RemoveAll(s => skills.Contains(s.Skill));
if (skillList.Count == 0) skills.Clear();
}
var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode);
var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode, IsVip(player));
if (gameMode == Config.GameModes.NoRepeat)
{
@ -374,6 +388,7 @@ namespace src.player
if (def == null) return false;
if (def.DisableOnPistolRound && SkillUtils.IsPistolRound()) return false;
if (def.NeedsTeammates && validPlayers.Count(p => p.Team == player.Team) == 1) return false;
if (def.MinPlayer > 0 && validPlayers.Count < def.MinPlayer) return false;
if (def.MaxPerServer >= 0 && assignmentCounts.TryGetValue(pick.Skill, out var c) && c >= def.MaxPerServer) return false;
return true;
@ -672,6 +687,10 @@ namespace src.player
if (SkillUtils.IsPistolRound())
skillList.RemoveAll(s => SkillsInfo.GetValue<bool>(s.Skill, "disableOnPistolRound"));
SkillsInfo.DefaultSkillInfo[] skillsMinPlayer = [.. SkillsInfo.LoadedConfig.Where(s => s.MinPlayer > 0 && validPlayers.Count < s.MinPlayer)];
if (skillsMinPlayer.Length != 0)
skillList.RemoveAll(s => skillsMinPlayer.Any(s2 => s2.Name == s.Skill.ToString()));
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out HashSet<Skills>? skills))
{
skillList.RemoveAll(s => skills.Contains(s.Skill));
@ -686,7 +705,7 @@ namespace src.player
else assignmentCounts[sp.Skill] = 1;
}
randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode);
randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode, IsVip(player));
}
else if (gameMode == Config.GameModes.TeamSkills)
randomSkill = player.Team == CsTeam.Terrorist ? tSkill : ctSkill;

View file

@ -1,5 +1,4 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
@ -86,7 +85,7 @@ namespace src.player.skills
{
if (player == null || !player.IsValid) return;
int flashbangLimit = ConVar.Find("ammo_grenade_limit_flashbang")?.GetPrimitiveValue<int>() ?? 2;
int flashbangLimit = SkillUtils.CvarValue("ammo_grenade_limit_flashbang", 2);
int grenadeLimit = SkillsInfo.GetValue<int>(skillName, "grenadeLimit");
if (grenadeLimit > flashbangLimit)

View file

@ -179,7 +179,7 @@ namespace src.player.skills
damageInfo.Damage *= SkillUtils.GetTeamDamageMultiplier(skillName);
}
if (owner != null && damageInfo.Damage >= victimPawn.Health)
if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn))
SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion);
}

View file

@ -1,6 +1,5 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
@ -152,7 +151,7 @@ namespace src.player.skills
killerEvent.PrintToChat($" {ChatColors.Green}{killerEvent.GetTranslation("bounty_claimed_info", victim.PlayerName, reward)}");
}
private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue<int>() ?? 16000;
private static int GetMaxMoney() => SkillUtils.CvarValue("mp_maxmoney", 16000);
private static bool GiveMoney(CCSPlayerController player, int amount)
{

View file

@ -183,8 +183,8 @@ namespace src.player.skills
Vector newPos = new(playerPawn.AbsOrigin.X, playerPawn.AbsOrigin.Y, playerPawn.AbsOrigin.Z + 30);
QAngle newAngle = new(playerPawn.AbsRotation.X, playerPawn.AbsRotation.Y, playerPawn.AbsRotation.Z);
emptyProp.Teleport(newPos, newAngle);
emptyProp.DispatchSpawn();
emptyProp.Teleport(newPos, newAngle);
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");

View file

@ -21,6 +21,7 @@ namespace src.player.skills
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim) return;
if (!SkillUtils.FiresBullets(@event.Weapon)) return;
var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index);
if (Heavyweight.Resists(victim)) return;
@ -29,8 +30,9 @@ namespace src.player.skills
if (Instance.Random.NextDouble() <= attackerInfo.SkillChance)
{
var victimPawn = victim.PlayerPawn?.Value;
if (victimPawn != null)
victimPawn.AbsVelocity.Z = 300f;
if (victimPawn == null || !victimPawn.IsValid || victimPawn.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
victimPawn.AbsVelocity.Z = 300f;
}
}

View file

@ -84,7 +84,7 @@ namespace src.player.skills
float currentTime = Server.CurrentTime;
float extraTime = SkillsInfo.GetValue<float>(skillName, "bombArmedTime");
foreach (var player in PlayerManager.GetTickPlayers().Where(p => p.Team == CsTeam.Terrorist))
foreach (var player in PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid && p.Team == CsTeam.Terrorist))
{
if (player == null || !player.IsValid) continue;

View file

@ -128,7 +128,7 @@ namespace src.player.skills
damageInfo.Damage *= SkillUtils.GetTeamDamageMultiplier(skillName);
}
if (owner != null && damageInfo.Damage >= victimPawn.Health)
if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn))
SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion);
}

View file

@ -240,7 +240,7 @@ namespace src.player.skills
damageInfo.Damage *= SkillUtils.GetTeamDamageMultiplier(skillName);
}
if (owner != null && damageInfo.Damage >= victimPawn.Health)
if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn))
SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion);
}

View file

@ -15,13 +15,13 @@ namespace src.player.skills
private const Skills skillName = Skills.ExplosiveShot;
private static readonly QAngle angle = new(5, 10, -4);
private static int lastTick = 0;
private static byte pendingTeam = (byte)CsTeam.None;
private static readonly ConcurrentDictionary<int, (byte Team, uint Owner)> nades = [];
private static readonly ConcurrentDictionary<uint, int> lastTickByPlayer = [];
private static readonly ConcurrentDictionary<int, ConcurrentQueue<(byte Team, uint Owner)>> nades = [];
public static void NewRound()
{
nades.Clear();
lastTickByPlayer.Clear();
}
public static void LoadSkill()
@ -43,9 +43,8 @@ namespace src.player.skills
private static void SpawnExplosion(Vector vector, CCSPlayerController player)
{
lastTick = Server.TickCount;
pendingTeam = player.TeamNum;
nades.AddOrUpdate(Server.TickCount, (player.TeamNum, player.Index), (_, _) => (player.TeamNum, player.Index));
lastTickByPlayer[player.Index] = Server.TickCount;
nades.GetOrAdd(Server.TickCount, static _ => new ConcurrentQueue<(byte, uint)>()).Enqueue((player.TeamNum, player.Index));
SkillUtils.CreateHEGrenadeProjectile(vector, angle, new Vector(0, 0, 0), player.TeamNum);
}
@ -64,14 +63,16 @@ namespace src.player.skills
if (!(NearlyEquals(angle.X, heProjectile.AbsRotation.X) && NearlyEquals(angle.Y, heProjectile.AbsRotation.Y) && NearlyEquals(angle.Z, heProjectile.AbsRotation.Z)))
return;
if (!nades.TryGetValue(spawnTick, out var queue) || !queue.TryDequeue(out var source)) return;
if (queue.IsEmpty) nades.TryRemove(spawnTick, out _);
heProjectile.TicksAtZeroVelocity = 100;
heProjectile.TeamNum = pendingTeam;
heProjectile.TeamNum = source.Team;
heProjectile.Damage = SkillsInfo.GetValue<float>(skillName, "damage");
heProjectile.DmgRadius = SkillsInfo.GetValue<float>(skillName, "damageRadius");
heProjectile.DetonateTime = 0;
if (nades.TryRemove(spawnTick, out var source))
heProjectile.Globalname = $"explosiveshot_team_{source.Team}_{source.Owner}_{heProjectile.Index}";
heProjectile.Globalname = $"explosiveshot_team_{source.Team}_{source.Owner}_{heProjectile.Index}";
});
}
@ -112,7 +113,7 @@ namespace src.player.skills
var owner = Utilities.GetPlayerFromIndex((int)ownerIndex);
if (owner != null && !owner.IsValid) owner = null;
if (owner != null && damageInfo.Damage >= victimPawn.Health)
if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn))
SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion);
}
@ -120,11 +121,12 @@ namespace src.player.skills
public static void BulletImpact(EventBulletImpact @event)
{
if (lastTick == Server.TickCount) return;
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
// One explosion per player per tick: a shotgun reports every pellet as its own impact.
if (lastTickByPlayer.TryGetValue(player.Index, out int last) && last == Server.TickCount) return;
var pos = new Vector(@event.X, @event.Y, @event.Z);
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -11,9 +11,7 @@ namespace src.player.skills
{
private const Skills skillName = Skills.FireRain;
private static readonly ConcurrentDictionary<uint, byte> decoys = [];
private static int rainTick = -1;
private static CCSPlayerPawn? rainThrower;
private static byte rainTeam = (byte)CsTeam.None;
private static readonly ConcurrentDictionary<int, ConcurrentQueue<RainBatch>> rainBatches = [];
private static readonly ConcurrentDictionary<uint, (uint ThrowerRaw, byte Team)> rainMolotovs = [];
public static void LoadSkill()
@ -25,6 +23,7 @@ namespace src.player.skills
public static void NewRound()
{
KillAllDecoys();
rainBatches.Clear();
rainMolotovs.Clear();
DecoyRing.ClearAll(skillName);
}
@ -54,10 +53,6 @@ namespace src.player.skills
const float spawnHeight = 1500.0f;
const float approachDistance = 600.0f;
rainTick = Server.TickCount;
rainThrower = pawn;
rainTeam = player.TeamNum;
float startAngle = Random.Shared.NextSingle() * MathF.Tau;
Vector? skyCenter = null;
@ -88,10 +83,12 @@ namespace src.player.skills
if (!foundPosition || skyCenter == null)
{
QueueRain(pawn, player.TeamNum, grenadeGroundCount);
CreateMolotovSplash(targetPos, grenadeGroundCount, player.TeamNum);
return;
}
QueueRain(pawn, player.TeamNum, grenadeCount);
CreateMolotovRaid(targetPos, skyCenter, grenadeCount, player.TeamNum);
}
@ -202,24 +199,45 @@ namespace src.player.skills
}
}
private sealed class RainBatch
{
public required CCSPlayerPawn Thrower;
public required byte Team;
public int Remaining;
}
private static void QueueRain(CCSPlayerPawn thrower, byte team, int count)
{
rainBatches.GetOrAdd(Server.TickCount, static _ => new ConcurrentQueue<RainBatch>())
.Enqueue(new RainBatch { Thrower = thrower, Team = team, Remaining = count });
}
private static void ConsumeOne(ConcurrentQueue<RainBatch> queue, RainBatch batch)
{
if (--batch.Remaining > 0) return;
queue.TryDequeue(out _);
}
public static void OnEntitySpawned(CEntityInstance entity)
{
var name = entity.DesignerName;
if (name == "molotov_projectile")
{
if (Server.TickCount != rainTick) return;
if (!rainBatches.TryGetValue(Server.TickCount, out var queue) || !queue.TryPeek(out var batch)) return;
var thrower = rainThrower;
if (thrower == null || !thrower.IsValid) return;
var thrower = batch.Thrower;
if (thrower == null || !thrower.IsValid) { ConsumeOne(queue, batch); return; }
var molotov = entity.As<CMolotovProjectile>();
if (molotov == null || !molotov.IsValid) return;
if (molotov == null || !molotov.IsValid) { ConsumeOne(queue, batch); return; }
molotov.TeamNum = rainTeam;
ConsumeOne(queue, batch);
molotov.TeamNum = batch.Team;
molotov.Thrower.Raw = thrower.EntityHandle.Raw;
molotov.OwnerEntity.Raw = thrower.EntityHandle.Raw;
rainMolotovs[molotov.Index] = (thrower.EntityHandle.Raw, rainTeam);
rainMolotovs[molotov.Index] = (thrower.EntityHandle.Raw, batch.Team);
Server.NextWorldUpdate(() =>
{

View file

@ -1,6 +1,5 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
@ -32,7 +31,7 @@ namespace src.player.skills
if (!SkillsInfo.GetValue<bool>(skillName, "manageAutoKick")) return false;
bool live;
try { live = ConVar.Find("mp_autokick")?.GetPrimitiveValue<bool>() ?? false; }
try { live = SkillUtils.CvarValue("mp_autokick", false); }
catch { return false; }
if (!live) return false;

View file

@ -138,6 +138,11 @@ namespace src.player.skills
else
skillList.RemoveAll(s => Event.terroristSkills.Any(s2 => s2.Name == s.Skill.ToString()));
int playerCount = PlayerManager.GetTickPlayers().Count(p => p != null && p.IsValid && !p.IsHLTV && (p.Team == CsTeam.Terrorist || p.Team == CsTeam.CounterTerrorist));
SkillsInfo.DefaultSkillInfo[] skillsMinPlayer = SkillsInfo.LoadedConfig.Where(s => s.MinPlayer > 0 && playerCount < s.MinPlayer).ToArray();
if (skillsMinPlayer.Length != 0)
skillList.RemoveAll(s => skillsMinPlayer.Any(s2 => s2.Name == s.Skill.ToString()));
return skillList.Count == 0 ? [Event.noneSkill] : skillList;
}

View file

@ -127,8 +127,8 @@ namespace src.player.skills
Vector newPos = new(playerPawn.AbsOrigin.X, playerPawn.AbsOrigin.Y, playerPawn.AbsOrigin.Z + 30);
QAngle newAngle = new(playerPawn.AbsRotation.X, playerPawn.AbsRotation.Y, playerPawn.AbsRotation.Z);
emptyProp.Teleport(newPos, newAngle);
emptyProp.DispatchSpawn();
emptyProp.Teleport(newPos, newAngle);
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");

View file

@ -150,6 +150,27 @@ namespace src.player.skills
}
}
public static void WeaponEquip(EventItemEquip @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
if (playersWithSkill.TryGetValue(player.Index, out int grenadesLeft) && grenadesLeft > 1)
SkillUtils.UpdateGrenadeCount(player, CsItem.DecoyGrenade, grenadesLeft);
}
public static void WeaponPickup(EventItemPickup @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
var weapon = @event.Item;
if (string.IsNullOrEmpty(weapon) || weapon != "decoy") return;
if (playersWithSkill.TryGetValue(player.Index, out int grenadesLeft) && grenadesLeft > 1)
SkillUtils.UpdateGrenadeCount(player, CsItem.DecoyGrenade, grenadesLeft);
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;

View file

@ -1,5 +1,4 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using jRandomSkills.src.utils;
@ -79,7 +78,7 @@ namespace src.player.skills
{
if (player == null || !player.IsValid) return;
int flashbangLimit = ConVar.Find("ammo_grenade_limit_flashbang")?.GetPrimitiveValue<int>() ?? 2;
int flashbangLimit = SkillUtils.CvarValue("ammo_grenade_limit_flashbang", 2);
int grenadeLimit = SkillsInfo.GetValue<int>(skillName, "grenadeLimit");
if (grenadeLimit > flashbangLimit)

View file

@ -54,7 +54,7 @@ namespace src.player.skills
if (playerInfo == null || playerInfo.Skill != skillName) continue;
if (!SkillUtils.HasMenu(player)) continue;
var enemies = PlayerManager.GetTickPlayers().Where(p => p.PawnIsAlive && p.Team != player.Team && p.IsValid && p.Team != CsTeam.Spectator && p.Team != CsTeam.None).ToArray();
var enemies = PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid && p.PawnIsAlive && p.Team != player.Team && p.Team != CsTeam.Spectator && p.Team != CsTeam.None).ToArray();
ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString())));
SkillUtils.UpdateMenu(player, menuItems);

View file

@ -156,8 +156,8 @@ namespace src.player.skills
Vector newPos = new(playerPawn.AbsOrigin.X, playerPawn.AbsOrigin.Y, playerPawn.AbsOrigin.Z + 30);
QAngle newAngle = new(playerPawn.AbsRotation.X, playerPawn.AbsRotation.Y, playerPawn.AbsRotation.Z);
emptyProp.Teleport(newPos, newAngle);
emptyProp.DispatchSpawn();
emptyProp.Teleport(newPos, newAngle);
Utilities.SetStateChanged(emptyProp, "CBaseEntity", "m_CBodyComponent");

View file

@ -1,6 +1,5 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
@ -20,7 +19,7 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
defaultNoSpread = ConVar.Find("weapon_accuracy_nospread")?.GetPrimitiveValue<bool>() ?? false;
defaultNoSpread = SkillUtils.CvarValue("weapon_accuracy_nospread", false);
}
public static void NewRound()

View file

@ -11,7 +11,6 @@ namespace src.player.skills
public class Phoenix : ISkill
{
private const Skills skillName = Skills.Phoenix;
private const float DefaultHeadshotMultiplier = 4f;
private static readonly ConcurrentDictionary<uint, int> phoenixTicks = new();
public static void LoadSkill()
@ -53,11 +52,7 @@ namespace src.player.skills
if (SkillUtils.IsFriendlyFireBlocked(damageInfo, victimPawn)) return;
float effectiveDamage = damageInfo.Damage;
if (SkillUtils.GetHitGroup(damageInfo) == HitGroup_t.HITGROUP_HEAD)
effectiveDamage *= GetHeadshotMultiplier(damageInfo);
if (effectiveDamage < victimPawn.Health) return;
if (!SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) return;
if (TryConsumeRevive(victim, victimPawn))
damageInfo.Damage = 0;
@ -99,20 +94,6 @@ namespace src.player.skills
return true;
}
private static float GetHeadshotMultiplier(CTakeDamageInfo info)
{
var ability = info.Ability?.Value;
if (ability == null || !ability.IsValid) return DefaultHeadshotMultiplier;
var weapon = ability.As<CCSWeaponBase>();
if (weapon == null || !weapon.IsValid) return DefaultHeadshotMultiplier;
var vdata = weapon.GetVData<CCSWeaponBaseVData>();
if (vdata == null || vdata.HeadshotMultiplier <= 0) return DefaultHeadshotMultiplier;
return vdata.HeadshotMultiplier;
}
public static void EnableSkill(CCSPlayerController player)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -1,6 +1,5 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
@ -20,7 +19,7 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue<int>() ?? 40;
defaultC4Timer = SkillUtils.CvarValue("mp_c4timer", 40);
}
public static void EnableSkill(CCSPlayerController player)
@ -96,7 +95,7 @@ namespace src.player.skills
float currentTime = Server.CurrentTime;
bool hudFrame = SkillUtils.IsHudFrame();
foreach (var player in PlayerManager.GetTickPlayers().Where(p => p.Team == CsTeam.Terrorist))
foreach (var player in PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid && p.Team == CsTeam.Terrorist))
{
if (!Instance.IsPlayerValid(player)) continue;

View file

@ -23,6 +23,8 @@ namespace src.player.skills
if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim)
return;
if (!SkillUtils.FiresBullets(@event.Weapon)) return;
if (Heavyweight.Resists(victim)) return;
var playerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index);

View file

@ -11,7 +11,6 @@ namespace src.player.skills
public class ReZombie : ISkill
{
private const Skills skillName = Skills.ReZombie;
private const float DefaultHeadshotMultiplier = 4f;
private static readonly ConcurrentDictionary<uint, int> zombies = [];
private static readonly object setLock = new();
@ -88,11 +87,7 @@ namespace src.player.skills
// Friendly-fire-off teammate hit deals 0 damage; don't zombify over a hit that never lands.
if (SkillUtils.IsFriendlyFireBlocked(damageInfo, victimPawn)) return;
float effectiveDamage = damageInfo.Damage;
if (SkillUtils.GetHitGroup(damageInfo) == HitGroup_t.HITGROUP_HEAD)
effectiveDamage *= GetHeadshotMultiplier(damageInfo);
if (effectiveDamage < victimPawn.Health) return; // survivable, let it through
if (!SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) return; // survivable, let it through
if (TryBecomeZombie(victim, victimPawn))
damageInfo.Damage = 0;
@ -138,20 +133,6 @@ namespace src.player.skills
holdTime: 600);
}
private static float GetHeadshotMultiplier(CTakeDamageInfo info)
{
var ability = info.Ability?.Value;
if (ability == null || !ability.IsValid) return DefaultHeadshotMultiplier;
var weapon = ability.As<CCSWeaponBase>();
if (weapon == null || !weapon.IsValid) return DefaultHeadshotMultiplier;
var vdata = weapon.GetVData<CCSWeaponBaseVData>();
if (vdata == null || vdata.HeadshotMultiplier <= 0) return DefaultHeadshotMultiplier;
return vdata.HeadshotMultiplier;
}
private static void DropAllBotWeapons(CCSPlayerController player)
{
if (player == null || !player.IsValid || !player.IsBot) return;

View file

@ -1,8 +1,8 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
using static src.jRandomSkills;
namespace src.player.skills
@ -11,48 +11,49 @@ namespace src.player.skills
{
private const Skills skillName = Skills.RichBoy;
private static readonly ConcurrentDictionary<uint, int> accountBeforeBonus = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue<int>() ?? 16000;
private static int GetMaxMoney() => SkillUtils.CvarValue("mp_maxmoney", 16000);
public static void EnableSkill(CCSPlayerController player)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
int moneyBonus = Instance.Random.Next(SkillsInfo.GetValue<int>(skillName, "minMoney"), SkillsInfo.GetValue<int>(skillName, "maxMoney"));
if (player == null || !player.IsValid) return;
var moneyServices = player.InGameMoneyServices;
if (moneyServices == null) return;
int moneyBonus = Instance.Random.Next(SkillsInfo.GetValue<int>(skillName, "minMoney"), SkillsInfo.GetValue<int>(skillName, "maxMoney"));
moneyBonus = Math.Min(moneyBonus, GetMaxMoney() - moneyServices.Account);
if (moneyBonus <= 0) return;
playerInfo.SkillChance = moneyBonus;
AddMoney(player, moneyBonus);
accountBeforeBonus[player.Index] = moneyServices.Account;
moneyServices.Account += moneyBonus;
Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices");
}
public static void DisableSkill(CCSPlayerController player)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
if (player == null || !player.IsValid) return;
if (!accountBeforeBonus.TryRemove(player.Index, out int accountBefore)) return;
var moneyServices = player.InGameMoneyServices;
if (moneyServices == null) return;
int money = Math.Abs((int)playerInfo.SkillChance! - moneyServices.CashSpentThisRound);
AddMoney(player, -money, 3000);
if (moneyServices.Account <= accountBefore) return;
moneyServices.Account = accountBefore;
Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices");
}
private static void AddMoney(CCSPlayerController player, int money, int minimum = 0)
public static void PlayerDisconnect(uint playerIndex)
{
if (player == null || !player.IsValid) return;
var moneyServices = player.InGameMoneyServices;
if (moneyServices == null) return;
moneyServices.Account = Math.Clamp(moneyServices.Account + money, minimum, GetMaxMoney());
Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices");
accountBeforeBonus.TryRemove(playerIndex, out _);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#D4AF37", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, int minMoney = 5000, int maxMoney = 15000) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)

View file

@ -1,6 +1,5 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using static src.jRandomSkills;
@ -16,7 +15,7 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue<int>() ?? 16000;
private static int GetMaxMoney() => SkillUtils.CvarValue("mp_maxmoney", 16000);
public static void PlayerHurt(EventPlayerHurt @event)
{

View file

@ -28,6 +28,7 @@ namespace src.player.skills
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim) return;
if (!SkillUtils.FiresBullets(@event.Weapon)) return;
var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index);
if (Heavyweight.Resists(victim)) return;

View file

@ -11,7 +11,6 @@ namespace src.player.skills
public class SecondLife : ISkill
{
private const Skills skillName = Skills.SecondLife;
private const float DefaultHeadshotMultiplier = 4f;
private static readonly ConcurrentDictionary<nint, int> usedThisRound = [];
private static readonly object setLock = new();
@ -45,11 +44,7 @@ namespace src.player.skills
if (SkillUtils.IsFriendlyFireBlocked(damageInfo, victimPawn)) return;
float effectiveDamage = damageInfo.Damage;
if (SkillUtils.GetHitGroup(damageInfo) == HitGroup_t.HITGROUP_HEAD)
effectiveDamage *= GetHeadshotMultiplier(damageInfo);
if (effectiveDamage < victimPawn.Health) return;
if (!SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) return;
if (usedThisRound.TryGetValue(victim.Handle, out int savedTick))
{
@ -62,20 +57,6 @@ namespace src.player.skills
damageInfo.Damage = 0;
}
private static float GetHeadshotMultiplier(CTakeDamageInfo info)
{
var ability = info.Ability?.Value;
if (ability == null || !ability.IsValid) return DefaultHeadshotMultiplier;
var weapon = ability.As<CCSWeaponBase>();
if (weapon == null || !weapon.IsValid) return DefaultHeadshotMultiplier;
var vdata = weapon.GetVData<CCSWeaponBaseVData>();
if (vdata == null || vdata.HeadshotMultiplier <= 0) return DefaultHeadshotMultiplier;
return vdata.HeadshotMultiplier;
}
public static bool TryConsumeRevive(CCSPlayerController? victim, CCSPlayerPawn? victimPawn)
{
if (victim == null || !victim.IsValid) return false;

View file

@ -32,6 +32,7 @@ namespace src.player.skills
if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim)) return;
if (attacker!.Index == victim!.Index) return;
if (!SkillUtils.FiresBullets(@event.Weapon)) return;
var victimInfo = PlayerManager.GetPlayerByIndex(victim!.Index);
var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index);

View file

@ -1,6 +1,5 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using static src.jRandomSkills;
@ -17,7 +16,7 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue<int>() ?? 40;
defaultC4Timer = SkillUtils.CvarValue("mp_c4timer", 40);
}
public static void EnableSkill(CCSPlayerController player)

View file

@ -75,25 +75,27 @@ namespace src.player.skills
}
}
if (emitter == null)
{
foreach (var p in PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid))
um.Recipients.Remove(p);
return;
}
List<CCSPlayerController> allowed = [];
foreach (var recipient in PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid))
{
bool hasSkill = SkillPlayerInfo.ContainsKey(recipient.Index);
if (emitter != null)
foreach (var recipient in um.Recipients)
{
if (recipient == null || !recipient.IsValid) continue;
if (recipient.Team == emitter.Team) continue;
var bot = PlayerManager.GetPlayerEvent(recipient);
bool botSkill = bot != null && SkillPlayerInfo.ContainsKey(bot.Index);
bool hasSkill = SkillPlayerInfo.ContainsKey(recipient.Index);
bool isTeammate = recipient.Team == emitter.Team;
var bot = PlayerManager.GetPlayerEvent(recipient);
bool botSkill = bot != null && bot.IsValid && SkillPlayerInfo.ContainsKey(bot.Index);
if ((!hasSkill && !botSkill) || isTeammate)
um.Recipients.Remove(recipient);
}
if (!hasSkill && !botSkill) continue;
allowed.Add(recipient);
}
um.Recipients.Clear();
foreach (var recipient in allowed)
um.Recipients.Add(recipient);
}
public static void OnTick()

View file

@ -26,6 +26,7 @@ namespace src.player.skills
}
private static bool roundEnded;
private static readonly ConcurrentDictionary<uint, byte> owedKnife = [];
public static void NewRound()
{
@ -36,13 +37,27 @@ namespace src.player.skills
public static void RoundEnd()
{
roundEnded = true;
KillAllKnives();
KillAllKnives(rememberOwed: true);
foreach (var player in PlayerManager.GetTickPlayers())
{
if (player == null || !player.IsValid || !player.PawnIsAlive) continue;
if (PlayerManager.GetPlayerByIndex(player.Index)?.Skill != skillName) continue;
if (CheckHasKnife(player)) continue;
owedKnife[player.Index] = 0;
}
}
private static void KillAllKnives()
private static void KillAllKnives(bool rememberOwed = false)
{
foreach (var knifeInfo in knivesInfo.Values.ToArray())
{
if (rememberOwed && knifeInfo.IsDropped)
owedKnife[knifeInfo.PlayerIndex] = 0;
DisableKnifeSkill(knifeInfo);
}
knivesInfo.Clear();
}
@ -155,18 +170,69 @@ namespace src.player.skills
public static void DisableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
bool wasDropped = owedKnife.ContainsKey(player.Index);
if (knivesInfo.TryRemove(player.Index, out KnifeInfo? knifeInfo) && knifeInfo != null)
{
bool wasDropped = knifeInfo.IsDropped;
wasDropped |= knifeInfo.IsDropped;
DisableKnifeSkill(knifeInfo);
if (wasDropped && player != null && player.IsValid && player.PawnIsAlive)
player.GiveNamedItem("weapon_knife");
}
if (!wasDropped) return;
if (!player.PawnIsAlive)
{
owedKnife[player.Index] = 0;
ScheduleRepay(player.Index, 0);
return;
}
if (!CheckHasKnife(player))
player.GiveNamedItem("weapon_knife");
owedKnife.TryRemove(player.Index, out _);
}
private const int RepayAttempts = 10;
private static void ScheduleRepay(uint playerIndex, int attempt)
{
if (attempt >= RepayAttempts)
{
owedKnife.TryRemove(playerIndex, out _);
return;
}
Instance.AddTimer(1f, () =>
{
if (!owedKnife.ContainsKey(playerIndex)) return;
var player = Utilities.GetPlayerFromIndex((int)playerIndex);
if (player == null || !player.IsValid)
{
owedKnife.TryRemove(playerIndex, out _);
return;
}
if (!player.PawnIsAlive)
{
ScheduleRepay(playerIndex, attempt + 1);
return;
}
if (!CheckHasKnife(player))
player.GiveNamedItem("weapon_knife");
owedKnife.TryRemove(playerIndex, out _);
}, TimerFlags.STOP_ON_MAPCHANGE);
}
public static void PlayerDisconnect(uint playerIndex)
{
owedKnife.TryRemove(playerIndex, out _);
if (knivesInfo.TryRemove(playerIndex, out KnifeInfo? knifeInfo) && knifeInfo != null)
DisableKnifeSkill(knifeInfo);
}

View file

@ -12,7 +12,7 @@ namespace src.player.skills
private const Skills skillName = Skills.TrueArmor;
private static readonly ConcurrentDictionary<uint, byte> holders = [];
private static readonly ConcurrentDictionary<uint, int> pending = [];
private static readonly ConcurrentDictionary<uint, PendingArmor> pending = [];
public static void LoadSkill()
{
@ -62,13 +62,16 @@ namespace src.player.skills
info.Damage = damage - absorbed;
pending[victim.Index] = (int)MathF.Round(armor - absorbed);
int restoredArmor = (int)MathF.Round(armor - absorbed);
bool hadHelmet = victimPawn.ItemServices?.As<CCSPlayer_ItemServices>()?.HasHelmet ?? false;
pending[victim.Index] = new PendingArmor(restoredArmor, hadHelmet);
victimPawn.ArmorValue = 0;
Utilities.SetStateChanged(victimPawn, "CCSPlayerPawn", "m_ArmorValue");
Debug.WriteToDebug($"[TrueArmor] {victim.PlayerName}: raw={damage:0.#} absorbed={absorbed:0.#} " +
$"passed={info.Damage:0.#} armor={armor}->{pending[victim.Index]} hp={victimPawn.Health}", DebugCategory.Damage);
$"passed={info.Damage:0.#} armor={armor}->{restoredArmor} helmet={hadHelmet} hp={victimPawn.Health}", DebugCategory.Damage);
}
public static void OnTakeDamagePost(CBaseEntity damagedEntity, CTakeDamageInfo damageInfo, CTakeDamageResult damageResult)
@ -76,12 +79,27 @@ namespace src.player.skills
if (pending.IsEmpty) return;
if (!TryResolveVictim(damagedEntity, damageInfo, out var victim, out var victimPawn, out _)) return;
if (!pending.TryRemove(victim!.Index, out int restored)) return;
if (!pending.TryRemove(victim!.Index, out var restored)) return;
victimPawn!.ArmorValue = restored;
victimPawn!.ArmorValue = restored.Armor;
Utilities.SetStateChanged(victimPawn, "CCSPlayerPawn", "m_ArmorValue");
if (!restored.HasHelmet) return;
var itemServices = victimPawn.ItemServices?.As<CCSPlayer_ItemServices>();
if (itemServices != null && !itemServices.HasHelmet)
itemServices.HasHelmet = true;
var owner = victimPawn.Controller.Value?.As<CCSPlayerController>();
if (owner != null && owner.IsValid && !owner.PawnHasHelmet)
{
owner.PawnHasHelmet = true;
Utilities.SetStateChanged(owner, "CCSPlayerController", "m_bPawnHasHelmet");
}
}
private readonly record struct PendingArmor(int Armor, bool HasHelmet);
private static bool TryResolveVictim(CBaseEntity damagedEntity, CTakeDamageInfo damageInfo, out CCSPlayerController? victim, out CCSPlayerPawn? pawn, out CTakeDamageInfo? info)
{
victim = null;

View file

@ -114,7 +114,8 @@ namespace src.player.skills
(WeaponInfo[]? playerWeapon, bool playerC4) = GetWeapons(player);
(WeaponInfo[]? enemyWeapon, bool enemyC4) = GetWeapons(enemy);
if (playerWeapon == null || !playerWeapon.Any(w => weapons.Contains(w.Name)))
if (playerWeapon == null || !playerWeapon.Any(w => weapons.Contains(w.Name))
|| enemyWeapon == null || !enemyWeapon.Any(w => weapons.Contains(w.Name)))
{
skillInfo.FindedEnemy = true;
skillInfo.HaveWeapon = false;
@ -268,7 +269,12 @@ namespace src.player.skills
private static CCSPlayerController? GetRandomEnemy(CCSPlayerController player)
{
CCSPlayerController[] enemies = [.. PlayerManager.GetTickPlayers().FindAll(e => e.Team != player.Team && e.PlayerPawn?.Value?.Health > 0)];
CCSPlayerController[] enemies = [.. PlayerManager.GetTickPlayers().FindAll(e =>
e != null && e.IsValid
&& e.Team != player.Team && e.Team != CsTeam.Spectator && e.Team != CsTeam.None
&& e.LifeState == (byte)LifeState_t.LIFE_ALIVE
&& e.PlayerPawn?.Value != null && e.PlayerPawn.Value.IsValid
&& e.PlayerPawn.Value.Health > 0)];
if (enemies.Length == 0) return null;
return enemies[Instance.Random.Next(enemies.Length)];
}

View file

@ -24,9 +24,11 @@ namespace src.utils
{
var newConfig = new SettingsModel();
Instance.Logger.LogInformation("config path: {Path} (exists={Exists})", configPath, File.Exists(configPath));
if (!File.Exists(configPath))
{
Instance.Logger.LogInformation("Config file does not exist. Create a new config file...");
Instance.Logger.LogError("config.json NOT FOUND at the path above; a fresh file with defaults is being written.");
SaveConfig(newConfig);
debugFlags = DebugCategories.Parse(newConfig.DebugMode);
return config = newConfig;
@ -43,19 +45,44 @@ namespace src.utils
if (HasMissingKeys(json) || IsLegacyDebugMode(json))
SaveConfig(newConfig);
}
catch
catch (Exception ex)
{
Instance.Logger.LogError("Error when loading the config file.");
Instance.Logger.LogError("Error when loading the config file: {Message}", ex.Message);
if (config != null)
{
Instance.Logger.LogError("config.json was not applied; the previously loaded settings are kept.");
return config;
}
}
if (newConfig.DisplayAlwaysDescription)
newConfig.SkillDescriptionDuration = 9999;
debugFlags = DebugCategories.Parse(newConfig.DebugMode);
ApplyRarityTables(newConfig);
return config = newConfig;
}
}
private static void ApplyRarityTables(SettingsModel model)
{
RarityManager.SetRarityPercentages(ToRarityTable(model.SkillsChance));
RarityManager.SetVipRarityPercentages(ToRarityTable(model.VIPSkillsChance));
}
private static Dictionary<Rarity, float> ToRarityTable(Dictionary<string, float>? source)
{
var table = new Dictionary<Rarity, float>();
if (source == null) return table;
foreach (var kv in source)
if (Enum.TryParse<Rarity>(kv.Key, true, out var rarity))
table[rarity] = kv.Value;
return table;
}
private static bool HasMissingKeys(string json)
{
try
@ -111,12 +138,11 @@ namespace src.utils
string tempPath = $"{configPath}.temp";
File.WriteAllText(tempPath, json);
File.Copy(tempPath, configPath, overwrite: true);
File.Delete(tempPath);
File.Move(tempPath, configPath, overwrite: true);
}
catch
catch (Exception ex)
{
Instance.Logger.LogError("Error when saving the config file.");
Instance.Logger.LogError("Error when saving the config file: {Message}", ex.Message);
}
}
}
@ -146,6 +172,11 @@ namespace src.utils
public bool TraceRayBeam { get; set; }
public string DisableHUDOnDeathPermission { get; set; }
public bool DisableSkillsOnRoundEnd { get; set; }
public string VIPFlag { get; set; }
[JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)]
public Dictionary<string, float> SkillsChance { get; set; }
[JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)]
public Dictionary<string, float> VIPSkillsChance { get; set; }
public int? CurseSkillPerPlayer { get; set; }
public bool ShowDecoyRing { get; set; }
public WeaponPools Weapons { get; set; }
@ -179,6 +210,25 @@ namespace src.utils
HideHudForOtherPlugins = true;
DisableHUDOnDeathPermission = "@jRandomSkills/death";
DisableSkillsOnRoundEnd = false;
VIPFlag = "@css/vip";
SkillsChance = new Dictionary<string, float>
{
["Common"] = 0.70f,
["Uncommon"] = 0.14f,
["Rare"] = 0.10f,
["Epic"] = 0.05f,
["Legendary"] = 0.01f,
};
VIPSkillsChance = new Dictionary<string, float>
{
["Common"] = 0.55f,
["Uncommon"] = 0.23f,
["Rare"] = 0.14f,
["Epic"] = 0.07f,
["Legendary"] = 0.01f,
};
CurseSkillPerPlayer = null;
ShowDecoyRing = true;

View file

@ -264,7 +264,8 @@ namespace src.utils
}
// Dying entities stay out of transmit until the engine processes the kill (Event.CheckTransmit).
private static readonly ConcurrentDictionary<uint, DateTime> recentlyDestroyed = new();
private readonly record struct DyingEntity(DateTime Expires, uint HandleRaw);
private static readonly ConcurrentDictionary<uint, DyingEntity> recentlyDestroyed = new();
public static List<uint> GetRecentlyDestroyedSnapshot()
{
@ -274,8 +275,20 @@ namespace src.utils
var result = new List<uint>();
foreach (var kvp in recentlyDestroyed)
{
if (now > kvp.Value) recentlyDestroyed.TryRemove(kvp.Key, out _);
else result.Add(kvp.Key);
if (now > kvp.Value.Expires)
{
recentlyDestroyed.TryRemove(kvp.Key, out _);
continue;
}
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)kvp.Key);
if (entity == null || !entity.IsValid || entity.EntityHandle.Raw != kvp.Value.HandleRaw)
{
recentlyDestroyed.TryRemove(kvp.Key, out _);
continue;
}
result.Add(kvp.Key);
}
return result;
}
@ -297,7 +310,7 @@ namespace src.utils
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)entityIndex);
if (entity != null && entity.IsValid)
{
recentlyDestroyed[entityIndex] = DateTime.UtcNow.AddSeconds(delay + 2.0);
recentlyDestroyed[entityIndex] = new DyingEntity(DateTime.UtcNow.AddSeconds(delay + 0.5), entity.EntityHandle.Raw);
// Detach first so no follower is left on a freed parent.
entity.AcceptInput("ClearParent");
entity.AddEntityIOEvent("Kill", entity, delay: delay);

View file

@ -23,6 +23,15 @@ namespace src.utils
{ Rarity.Legendary, 1f }
};
private static Dictionary<Rarity, float> vipRarityPercentages = new()
{
{ Rarity.Common, 55f },
{ Rarity.Uncommon, 23f },
{ Rarity.Rare, 14f },
{ Rarity.Epic, 7f },
{ Rarity.Legendary, 1f }
};
public static IReadOnlyDictionary<Rarity, float> RarityPercentages
{
get
@ -34,26 +43,39 @@ namespace src.utils
public static void SetRarityPercentages(IDictionary<Rarity, float> percentages)
{
if (percentages == null || percentages.Count == 0) return;
var table = Normalize(percentages);
if (table == null) return;
lock (rarityLock)
{
double sum = percentages.Values.Sum(v => (double)v);
if (sum <= 0)
return;
rarityPercentages = table;
}
if (Math.Abs(sum - 100.0) > 0.0001)
{
var normalized = new Dictionary<Rarity, float>();
public static void SetVipRarityPercentages(IDictionary<Rarity, float> percentages)
{
var table = Normalize(percentages);
if (table == null) return;
foreach (var kv in percentages)
normalized[kv.Key] = (float)((kv.Value / sum) * 100.0);
lock (rarityLock)
vipRarityPercentages = table;
}
rarityPercentages = normalized;
}
else
rarityPercentages = percentages.ToDictionary(k => k.Key, v => v.Value);
}
// Accepts either percentages (70, 14, ...) or fractions (0.7, 0.14, ...); both are
// rescaled so the table sums to 100.
private static Dictionary<Rarity, float>? Normalize(IDictionary<Rarity, float> percentages)
{
if (percentages == null || percentages.Count == 0) return null;
double sum = percentages.Values.Sum(v => (double)v);
if (sum <= 0) return null;
if (Math.Abs(sum - 100.0) <= 0.0001)
return percentages.ToDictionary(k => k.Key, v => v.Value);
var normalized = new Dictionary<Rarity, float>();
foreach (var kv in percentages)
normalized[kv.Key] = (float)((kv.Value / sum) * 100.0);
return normalized;
}
public static float GetRarityPercentage(Rarity rarity)
@ -62,16 +84,18 @@ namespace src.utils
return rarityPercentages.TryGetValue(rarity, out var v) ? v : 0f;
}
public static (double, Rarity) RollRarity()
public static (double, Rarity) RollRarity(bool vip = false)
{
double roll = Random.Shared.NextDouble() * 100.0;
double accum = 0.0;
lock (rarityLock)
{
var table = vip ? vipRarityPercentages : rarityPercentages;
foreach (var r in Enum.GetValues(typeof(Rarity)).Cast<Rarity>())
{
float chance = rarityPercentages.TryGetValue(r, out var val) ? val : 0f;
float chance = table.TryGetValue(r, out var val) ? val : 0f;
accum += chance;
if (roll <= accum)
return (roll, r);

View file

@ -21,6 +21,32 @@ namespace src.utils
{
public static class SkillUtils
{
private static readonly ConcurrentDictionary<string, ConVar> cvarCache = [];
public static ConVar? Cvar(string name)
{
if (cvarCache.TryGetValue(name, out var cached)) return cached;
var cvar = ConVar.Find(name);
if (cvar != null) cvarCache[name] = cvar;
return cvar;
}
public static T CvarValue<T>(string name, T fallback) where T : unmanaged
{
var cvar = Cvar(name);
if (cvar == null) return fallback;
try { return cvar.GetPrimitiveValue<T>(); }
catch { return fallback; }
}
public static string CvarString(string name, string fallback)
{
var cvar = Cvar(name);
return cvar == null ? fallback : cvar.StringValue;
}
private static Lazy<T?> LazySig<T>(string name, Func<string, T> factory) where T : class =>
new(() =>
{
@ -82,9 +108,9 @@ namespace src.utils
try
{
if (ConVar.Find("mp_halftime")?.GetPrimitiveValue<bool>() != true) return false;
if (!CvarValue("mp_halftime", false)) return false;
int maxRounds = ConVar.Find("mp_maxrounds")?.GetPrimitiveValue<int>() ?? 0;
int maxRounds = CvarValue("mp_maxrounds", 0);
return maxRounds > 0 && maxRounds / 2 == gameRules.TotalRoundsPlayed;
}
catch
@ -122,10 +148,15 @@ namespace src.utils
if (ammo == 1) return;
uint weaponIndex = weapon.Value.Index;
jRandomSkills.Instance.AddTimer(.1f, () =>
{
if (weapon == null || !weapon.IsValid || weapon.Value == null || !weapon.Value.IsValid) return;
weapon.Value.Clip1 = 1;
var tracked = Utilities.GetEntityFromIndex<CBasePlayerWeapon>((int)weaponIndex);
if (tracked == null || !tracked.IsValid || tracked.DesignerName != itemString) return;
tracked.Clip1 = 1;
Utilities.SetStateChanged(tracked, "CBasePlayerWeapon", "m_iClip1");
}, TimerFlags.STOP_ON_MAPCHANGE);
}
@ -310,6 +341,62 @@ namespace src.utils
return (HitGroup_t)Marshal.ReadInt32(hitGroupData, 56);
}
private const float DefaultHeadshotMultiplier = 4f;
private const float StomachMultiplier = 1.25f;
private const float LegMultiplier = 0.75f;
public static float GetAppliedDamageScale(CTakeDamageInfo? info, CCSPlayerPawn? victimPawn)
{
if (info == null || victimPawn == null || !victimPawn.IsValid) return 1f;
var hitGroup = GetHitGroup(info);
var vdata = GetWeaponVData(info);
float scale = hitGroup switch
{
HitGroup_t.HITGROUP_HEAD => vdata != null && vdata.HeadshotMultiplier > 0 ? vdata.HeadshotMultiplier : DefaultHeadshotMultiplier,
HitGroup_t.HITGROUP_STOMACH => StomachMultiplier,
HitGroup_t.HITGROUP_LEFTLEG or HitGroup_t.HITGROUP_RIGHTLEG => LegMultiplier,
_ => 1f,
};
if (vdata == null || vdata.ArmorRatio <= 0 || vdata.ArmorRatio >= 1f) return scale;
if (victimPawn.ArmorValue <= 0) return scale;
if (!ArmorCovers(hitGroup, victimPawn)) return scale;
return scale * vdata.ArmorRatio;
}
public static float PredictAppliedDamage(CTakeDamageInfo? info, CCSPlayerPawn? victimPawn)
{
if (info == null) return 0f;
return info.Damage * GetAppliedDamageScale(info, victimPawn);
}
public static bool IsPredictedLethal(CTakeDamageInfo? info, CCSPlayerPawn? victimPawn)
{
if (victimPawn == null || !victimPawn.IsValid) return false;
return PredictAppliedDamage(info, victimPawn) >= victimPawn.Health;
}
private static bool ArmorCovers(HitGroup_t hitGroup, CCSPlayerPawn victimPawn) => hitGroup switch
{
HitGroup_t.HITGROUP_LEFTLEG or HitGroup_t.HITGROUP_RIGHTLEG => false,
HitGroup_t.HITGROUP_HEAD => victimPawn.ItemServices?.As<CCSPlayer_ItemServices>()?.HasHelmet ?? false,
_ => true,
};
private static CCSWeaponBaseVData? GetWeaponVData(CTakeDamageInfo info)
{
var ability = info.Ability?.Value;
if (ability == null || !ability.IsValid) return null;
var weapon = ability.As<CCSWeaponBase>();
if (weapon == null || !weapon.IsValid) return null;
return weapon.GetVData<CCSWeaponBaseVData>();
}
public static void CreateHEGrenadeProjectile(Vector pos, QAngle angle, Vector vel, int teamNum)
{
HEGrenadeProjectile_CreateFunc.Value?.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 44, teamNum);
@ -341,8 +428,8 @@ namespace src.utils
if (!attackerPawn.IsValid || attackerPawn.DesignerName != "player") return false; // non-player inflictor
if (attackerPawn.TeamNum != victimPawn.TeamNum) return false; // enemy -> real damage
bool ff = ConVar.Find("mp_friendlyfire")?.GetPrimitiveValue<bool>() ?? false;
bool tae = ConVar.Find("mp_teammates_are_enemies")?.GetPrimitiveValue<bool>() ?? false;
bool ff = CvarValue("mp_friendlyfire", false);
bool tae = CvarValue("mp_teammates_are_enemies", false);
return !ff && !tae; // same team + FF off -> engine will zero this damage
}
@ -351,8 +438,8 @@ namespace src.utils
if (!IsSameTeamHit(info, victimPawn)) return false;
if (!SkillsInfo.GetValue<bool>(skill, "friendlyFire")) return true;
bool ff = ConVar.Find("mp_friendlyfire")?.GetPrimitiveValue<bool>() ?? false;
bool tae = ConVar.Find("mp_teammates_are_enemies")?.GetPrimitiveValue<bool>() ?? false;
bool ff = CvarValue("mp_friendlyfire", false);
bool tae = CvarValue("mp_teammates_are_enemies", false);
return !ff && !tae;
}
@ -715,6 +802,7 @@ namespace src.utils
var pawn = controller.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid) continue;
if (pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE || pawn.Health <= 0) continue;
hidden.Add(new HiddenPawn(controller.Index, controller.Team, pawn, bombOwnerIndex == controller.Index, ResolveCarriedIndexes(pawn)));
}
@ -1199,17 +1287,17 @@ namespace src.utils
private static void AwardRoundEndMoney(CsTeam winnerTeam, CCSPlayerController? bonusPlayer = null)
{
int winnerReward = winnerTeam == CsTeam.CounterTerrorist
? ConVar.Find("cash_team_win_by_defusing_bomb")?.GetPrimitiveValue<int>() ?? 3500
: ConVar.Find("cash_team_terrorist_win_bomb")?.GetPrimitiveValue<int>() ?? 3500;
? CvarValue("cash_team_win_by_defusing_bomb", 3500)
: CvarValue("cash_team_terrorist_win_bomb", 3500);
if (winnerReward <= 0) return;
int maxMoney = ConVar.Find("mp_maxmoney")?.GetPrimitiveValue<int>() ?? 16000;
int maxMoney = CvarValue("mp_maxmoney", 16000);
int personalBonus = bonusPlayer == null
? 0
: winnerTeam == CsTeam.CounterTerrorist
? ConVar.Find("cash_player_defused_bomb")?.GetPrimitiveValue<int>() ?? 300
: ConVar.Find("cash_player_bomb_planted")?.GetPrimitiveValue<int>() ?? 300;
? CvarValue("cash_player_defused_bomb", 300)
: CvarValue("cash_player_bomb_planted", 300);
foreach (var player in PlayerManager.GetTickPlayers())
{
@ -1232,10 +1320,10 @@ namespace src.utils
{
if (jRandomSkills.Instance == null || jRandomSkills.Instance.GameRules == null) return;
int totalRoundsPlayed = ctScore + tScore;
int maxRounds = ConVar.Find("mp_maxrounds")?.GetPrimitiveValue<int>() ?? 24;
int maxRounds = CvarValue("mp_maxrounds", 24);
int halfRounds = maxRounds / 2;
int overtimeMaxRounds = ConVar.Find("mp_overtime_maxrounds")?.GetPrimitiveValue<int>() ?? 6;
int overtimeLimit = ConVar.Find("mp_overtime_limit")?.GetPrimitiveValue<int>() ?? 1;
int overtimeMaxRounds = CvarValue("mp_overtime_maxrounds", 6);
int overtimeLimit = CvarValue("mp_overtime_limit", 1);
var gameRulesProxy = jRandomSkills.Instance.GameRules;
gameRulesProxy.TotalRoundsPlayed = totalRoundsPlayed;

View file

@ -29,9 +29,12 @@ namespace src.utils
{
var newConfig = new SkillsInfoModel();
Instance.Logger.LogInformation("skillsInfo path: {Path} (exists={Exists}, defaults built={Built})",
configPath, File.Exists(configPath), newConfig.Count);
if (!File.Exists(configPath))
{
Instance.Logger.LogInformation("Config file does not exist. Create a new skills info file...");
Instance.Logger.LogError("skillsInfo.json NOT FOUND at the path above; a fresh file with defaults is being written.");
SaveConfig(newConfig);
return config = newConfig;
}
@ -45,6 +48,7 @@ namespace src.utils
var root = JsonConvert.DeserializeObject<JArray>(json);
bool needsRewrite = root == null;
bool populateFailed = false;
var present = new HashSet<string>(StringComparer.Ordinal);
if (root != null)
@ -61,21 +65,40 @@ namespace src.utils
if (skillObj is JObject current && HasMissingKeys(current, instance))
needsRewrite = true;
JsonConvert.PopulateObject(skillObj.ToString(), instance);
try
{
JsonConvert.PopulateObject(skillObj.ToString(), instance);
}
catch (Exception ex)
{
populateFailed = true;
Instance.Logger.LogError("skillsInfo.json: \"{Name}\" could not be read ({Message}); its defaults are kept.", name, ex.Message);
}
}
if (newConfig.Any(s => !string.IsNullOrEmpty(s.Name) && !present.Contains(s.Name)))
needsRewrite = true;
if (needsRewrite)
Instance.Logger.LogInformation("skillsInfo.json read: {Read} entries, {Matched} matched a known skill, rewrite={Rewrite}",
root?.Count ?? 0, present.Count, needsRewrite);
if (populateFailed)
Instance.Logger.LogError("skillsInfo.json was not rewritten because some entries failed to load; fix them before reloading.");
else if (needsRewrite)
{
SaveConfig(newConfig);
Instance.Logger.LogInformation("skillsInfo.json was missing keys; rewritten with the defaults filled in.");
}
}
catch
catch (Exception ex)
{
Instance.Logger.LogError("Error when loading the skills info file.");
Instance.Logger.LogError("Error when loading the skills info file: {Message}", ex.Message);
if (config != null)
{
Instance.Logger.LogError("skillsInfo.json was not applied; the previously loaded skill settings are kept.");
return config;
}
}
return config = newConfig;
@ -104,12 +127,11 @@ namespace src.utils
string tempPath = $"{configPath}.temp";
File.WriteAllText(tempPath, json);
File.Copy(tempPath, configPath, overwrite: true);
File.Delete(tempPath);
File.Move(tempPath, configPath, overwrite: true);
}
catch
catch (Exception ex)
{
Instance.Logger.LogError("Error when saving the skills info file.");
Instance.Logger.LogError("Error when saving the skills info file: {Message}", ex.Message);
}
}
}
@ -191,7 +213,7 @@ namespace src.utils
}
}
public class DefaultSkillInfo(Skills skill, bool active = true, string color = "#ffffff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, bool disableOnPistolRound = false)
public class DefaultSkillInfo(Skills skill, bool active = true, string color = "#ffffff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, bool disableOnPistolRound = false, int minPlayer = 0)
{
public bool NeedsTeammates { get; set; } = needsTeammates;
public bool DisableOnFreezeTime { get; set; } = disableOnFreezeTime;
@ -204,6 +226,7 @@ namespace src.utils
public float? DescriptionHudDuration { get; set; } = descriptionHudDuration;
public string RequiredPermission { get; set; } = requiredPermission;
public int MaxPerServer { get; set; } = maxPerServer;
public int MinPlayer { get; set; } = minPlayer;
public string Rarity { get; set; } = rarity.ToString();
}

View file

@ -21,6 +21,21 @@
"TraceRayBeam": false,
"DisableHUDOnDeathPermission": "@jRandomSkills/death",
"DisableSkillsOnRoundEnd": false,
"VIPFlag": "@css/vip",
"SkillsChance": {
"Common": 0.7,
"Uncommon": 0.14,
"Rare": 0.1,
"Epic": 0.05,
"Legendary": 0.01
},
"VIPSkillsChance": {
"Common": 0.55,
"Uncommon": 0.23,
"Rare": 0.14,
"Epic": 0.07,
"Legendary": 0.01
},
"CurseSkillPerPlayer": null,
"ShowDecoyRing": true,
"Weapons": {

View file

@ -82,7 +82,7 @@
"catapult": "Mancınık",
"catapult_desc": "Vurduğun rakibi uzaya (yukarı doğru) fırlatma şansın var",
"catapult_desc2": "Fırlatma şansın: %{0}",
"catapult_desc2": "Vurduğun rakibi havaya fırlatma şansın: %{0}",
"chameleon": "Bukalemun",
"chameleon_desc": "Öldürdüğün ilk oyuncunun yeteneği sana geçer",
@ -452,7 +452,7 @@
"push": "İtici",
"push_desc": "Düşmana vurduğunda onu geri itme şansın olur",
"push_desc2": "Geri itme şansın: %{0}",
"push_desc2": "Vurduğun rakibi geri itme şansın: %{0}",
"pyro": "Ateşbaz",
"pyro_desc": "Molotof canını yeniler",
@ -511,7 +511,7 @@
"shade": "Gölge",
"shade_desc": "Vurduğun düşmanın arkasına ışınlanma şansın olur",
"shade_desc2": "Arkasına ışınlanma şansın: %{0}",
"shade_desc2": "Vurduğun rakibin arkasına ışınlanma şansın: %{0}",
"shade_nospace": "Uygun alan yok",
"shortbomb": "Kısa Fünye",