v1.2.3.b6

New skills (9)
- Bounty: put a cash price on an enemy's head, the killer collects it.
- EmpGrenade: grenade victims lose radar and crosshair for a while.
- GravityDecoy: the decoy alters gravity for everyone in its radius.
- Heavyweight: immune to push and slow effects from other skills.
- Nemesis: the marked player takes extra damage from everyone.
- Punisher: banks part of the damage you take and releases it on your next hit.
- Rewind: drop a marker and teleport back to it shortly after.
- TrueArmor: armor absorbs part of incoming damage instead of health.
- Voodoo: the marked enemy shares the damage you take.

Shared systems
- DecoyRing: reusable ground ring so decoy skills show their real area of effect. Adopted by FireRain, FrozenDecoy, GravityDecoy and MagneticDecoy.
- WeaponPool: single cached weapon classification (rifle/pistol/grenade) with a normalizing lookup, backed by the new config Weapons section. Replaces the duplicated hardcoded arrays in Assassin, Chicken, Ghost, NoNades, PrimaryBan, RandomWeapon, SniperElite and WeaponsSwap. [because of compatibility with WeaponRestrict]
- HUD suppression registry in SkillUtils: crosshair and radar hiding is now refcounted per owner, so overlapping skills (Glitch, Jammer, EmpGrenade) cannot restore each other's state early.
- SkillUtils.ResolveHiddenPawns: shared invisible-pawn resolution for C4Camouflage, Ghost and Ninja.
- Config gained a Weapons section with automatic migration for existing installs, plus a per-skill DisableOnPistolRound switch honoured by both the round draw and css_setskill.

Sound
- Volume rebalance
- Add SkillUtils.EmitSoundToPlayer with a private RecipientFilter. A null filter had been broadcasting every skill sound to all clients. Migrated 16 call sites: Poison, DemonEye, ToxicSmoke, CarefulBullets, Thorns, HotBomb, LastGasp, LongKnife, Nightmare, Jester, Tripwire, Medic, HealingSmoke, Rewind, Watchmaker.
- New effects: LongKnife knife-hit feedback (Weapon_Knife.Hit.Heavy/Light), Grapple hook impact, Fortnite barricade placement, Ricochet bounce, Nightmare cursed tone, Replicator/Illusionist spawn, PsychicDefusing defuse cue, TakeAmmo pickup, Tripwire and Jammer trigger tones.
- 29 skills gained a configurable SoundVolume, LongKnife also HitSoundVolume.
- Grapple: emit the hook impact at the hook entity instead of at the owner so the enemy team can locate it.
- PawelJumper: the engine already plays the first jump, only extra jumps emit.

Models and visuals
- Grapple: real hook model with configurable hookScale and hookEmbed so the hook sits in the surface it hit.
- Pilot: glowing exhaust trail parented to the pawn, active only while airborne.

Performance
- SkillsInfo.GetValue now resolves through a ConcurrentDictionary cache instead of reflecting on every call; invalidated on config reload.
- HUD render cache keyed on player, config, description flag and the three source lines; identical frames skip the rebuild.
- Glaz.CheckTransmit: hoist smoke index resolution out of the per-viewer loop and build the pawn-owner map lazily.
- playersSkills switched from ConcurrentBag to HashSet<Skills>, turning the NoRepeat filter from an O(n) scan into a set lookup.

Fixes
- NoRepeat could hand a player two or three skills in one round because the history bag was never deduplicated; the HashSet rewrite closes it.
- SniperElite: restore the saved weapon after the halftime team swap instead of dropping it on the pistol round.
- Illiterate: the holder alert now reaches chat and is exempt from its own scrambler via PrintToChat(ignoreIlliterate).
- EntityManager.DestroyBeam: zero out width and render colour before the kill so beams cannot linger visibly, with a next-frame Remove fallback.
- Ricochet: drop all 18 diagnostic Trace calls and the Trace helper; the skill measured stable over 48h and the tracing dominated the debug log.

Debug
- Tag round start, round end and freeze-time-end lines with [WARMUP] so warmup load is separable from live rounds.
This commit is contained in:
ByDexter 2026-08-09 00:37:02 +03:00
parent 615c55b2da
commit b2d2c5bf9a
93 changed files with 3414 additions and 329 deletions

View file

@ -45,13 +45,13 @@ namespace src.player
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager").Where(t => t != null).ToList();
var tTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.Terrorist);
var ctTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.CounterTerrorist);
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score + 1} (CT {ctTeam?.Score} : {tTeam?.Score} TT) started.");
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score + 1} (CT {ctTeam?.Score} : {tTeam?.Score} TT) started.{WarmupTag()}");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventRoundFreezeEnd>((@event, info) =>
{
WriteToDebug($"Freeze time ended.");
WriteToDebug($"Freeze time ended.{WarmupTag()}");
return HookResult.Continue;
});
@ -60,7 +60,7 @@ namespace src.player
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager").Where(t => t != null).ToList();
var tTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.Terrorist);
var ctTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.CounterTerrorist);
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score} (CT {ctTeam?.Score} : {tTeam?.Score} TT) ended.");
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score} (CT {ctTeam?.Score} : {tTeam?.Score} TT) ended.{WarmupTag()}");
return HookResult.Continue;
});
@ -137,6 +137,13 @@ namespace src.player
return HookResult.Continue;
}
private static string WarmupTag()
{
var gameRules = Instance?.GameRules;
if (gameRules == null || gameRules.Handle == IntPtr.Zero) return " [gamerules unavailable]";
return gameRules.WarmupPeriod ? " [WARMUP]" : string.Empty;
}
public static void WriteToDebug(string message)
{
if (Config.LoadedConfig.DebugMode != true)

View file

@ -76,6 +76,7 @@ public enum Skills
BladeMaster,
BunnyHop,
BlastShot,
Bounty,
C4Camouflage,
CarefulBullets,
Catapult,
@ -95,6 +96,7 @@ public enum Skills
Dracula,
Duplicator,
Dwarf,
EmpGrenade,
EnemySpawn,
ExpensiveAmmo,
ExplodingBarrel,
@ -116,9 +118,11 @@ public enum Skills
Glue,
GodMode,
Grapple,
GravityDecoy,
Grenadier,
HealingChicken,
HealingSmoke,
Heavyweight,
Hermit,
HolyHandGrenade,
HomingNades,
@ -148,6 +152,7 @@ public enum Skills
Medic,
Miner,
MoneySwap,
Nemesis,
Nightmare,
Ninja,
NoNades,
@ -163,6 +168,7 @@ public enum Skills
Poison,
PrimaryBan,
Prosthesis,
Punisher,
Push,
Pyro,
QuickShot,
@ -175,6 +181,7 @@ public enum Skills
Replicator,
Retreat,
ReturnToSender,
Rewind,
RichBoy,
Ricochet,
RobinHood,
@ -199,6 +206,8 @@ public enum Skills
ThrowingKnife,
ToxicSmoke,
Tripwire,
TrueArmor,
Voodoo,
Wallhack,
Watchmaker,
WeaponsSwap,

View file

@ -33,7 +33,7 @@ namespace src.player
public static readonly SkillsInfo.DefaultSkillInfo[] counterterroristSkills = [.. SkillsInfo.LoadedConfig.Where(s => s.OnlyTeam == (int)CsTeam.CounterTerrorist)];
private static readonly SkillsInfo.DefaultSkillInfo[] allTeamsSkills = [.. SkillsInfo.LoadedConfig.Where(s => s.OnlyTeam == 0)];
private static readonly ConcurrentDictionary<uint, ConcurrentBag<jSkill_SkillInfo>> playersSkills = [];
private static readonly ConcurrentDictionary<uint, HashSet<Skills>> playersSkills = [];
public static readonly ConcurrentDictionary<uint, jSkill_SkillInfo> staticSkills = [];
public static readonly ConcurrentDictionary<uint, uint> playersCustomFOV = [];
private static readonly object setLock = new();
@ -564,6 +564,7 @@ namespace src.player
PlayerManager.UnregisterPlayer(player.Index);
EntityManager.DestroyPlayerEntities(player.Index);
SkillUtils.ClearHudSuppression(player.Index);
return HookResult.Continue;
}
@ -602,6 +603,8 @@ namespace src.player
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid) return;
SkillUtils.ClearHudSuppression(player.Index);
pawn.HideHUD = (uint)(pawn.HideHUD & ~(1 << 8));
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_iHideHUD");
@ -768,6 +771,21 @@ namespace src.player
}
var config = Config.LoadedConfig.HtmlHudCustomisation;
var cache = skillPlayer?.HudCache;
if (cache != null
&& cache.Content != null
&& cache.PlayerIndex == player.Index
&& ReferenceEquals(cache.Config, config)
&& cache.IsDescription == isDescription
&& string.Equals(cache.Header, headerLine, StringComparison.Ordinal)
&& string.Equals(cache.Center, centerLine, StringComparison.Ordinal)
&& string.Equals(cache.Extra, extraLine, StringComparison.Ordinal))
{
player.PrintToCenterHtml(cache.Content);
return;
}
var emptySymbol = $"<font class='fontSize-{(string.IsNullOrEmpty(headerLine) || string.IsNullOrEmpty(config.HeaderLineSize) ? "l" : "ml")}'> </font>";
var emptySymbol2 = $"<font class='fontSize-ml'> </font>";
@ -782,6 +800,19 @@ namespace src.player
: $"<br>{emptySymbol}<font class='fontSize-{(isDescription ? config.SkillDescriptionLineSize : config.InfoLineSize)}' color='{(isDescription ? config.SkillDescriptionLineColor : config.InfoLineColor)}'>{extraLine}</font>{emptySymbol}";
var hudContent = "<jRS/>" + infoLine + skillLine + remainingLine;
if (skillPlayer != null)
{
cache ??= skillPlayer.HudCache = new HudCacheEntry();
cache.PlayerIndex = player.Index;
cache.Config = config;
cache.Header = headerLine;
cache.Center = centerLine;
cache.Extra = extraLine;
cache.IsDescription = isDescription;
cache.Content = hudContent;
}
player.PrintToCenterHtml(hudContent);
}
}

View file

@ -195,6 +195,8 @@ namespace src.player
SkillsUsedThisMap.Clear();
nextRoundPicks.Clear();
SkillUtils.ClearAllHudSuppression();
playersCustomFOV.Clear();
playersSkills.Clear();
staticSkills.Clear();
@ -275,6 +277,7 @@ namespace src.player
public required HashSet<Skills> NeedsTeammates { get; init; }
public required HashSet<Skills> CtOnly { get; init; }
public required HashSet<Skills> TOnly { get; init; }
public required HashSet<Skills> PistolRoundBanned { get; init; }
public required int TerroristCount { get; init; }
public required int CounterTerroristCount { get; init; }
}
@ -296,6 +299,9 @@ namespace src.player
NeedsTeammates = ToSkillSet(SkillsInfo.LoadedConfig.Where(s => s.NeedsTeammates).Select(s => s.Name)),
CtOnly = ToSkillSet(counterterroristSkills.Select(s => s.Name)),
TOnly = ToSkillSet(terroristSkills.Select(s => s.Name)),
PistolRoundBanned = SkillUtils.IsPistolRound()
? ToSkillSet(SkillsInfo.LoadedConfig.Where(s => s.DisableOnPistolRound).Select(s => s.Name))
: [],
TerroristCount = validPlayers.Count(p => p.Team == CsTeam.Terrorist),
CounterTerroristCount = validPlayers.Count(p => p.Team == CsTeam.CounterTerrorist),
};
@ -328,9 +334,12 @@ namespace src.player
else
skillList.RemoveAll(s => ctx.TOnly.Contains(s.Skill));
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag<jSkill_SkillInfo>? skills))
if (ctx.PistolRoundBanned.Count != 0)
skillList.RemoveAll(s => ctx.PistolRoundBanned.Contains(s.Skill));
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out HashSet<Skills>? skills))
{
skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == s.Skill));
skillList.RemoveAll(s => skills.Contains(s.Skill));
if (skillList.Count == 0) skills.Clear();
}
@ -338,10 +347,10 @@ namespace src.player
if (gameMode == Config.GameModes.NoRepeat)
{
if (playersSkills.TryGetValue(player.Index, out ConcurrentBag<jSkill_SkillInfo>? value))
value.Add(randomSkill);
if (playersSkills.TryGetValue(player.Index, out HashSet<Skills>? value))
value.Add(randomSkill.Skill);
else
playersSkills.TryAdd(player.Index, [randomSkill]);
playersSkills.TryAdd(player.Index, [randomSkill.Skill]);
}
return randomSkill;
@ -358,6 +367,7 @@ namespace src.player
var def = SkillsInfo.LoadedConfig.FirstOrDefault(d => d.Name == name);
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.MaxPerServer >= 0 && assignmentCounts.TryGetValue(pick.Skill, out var c) && c >= def.MaxPerServer) return false;
@ -648,9 +658,12 @@ namespace src.player
else
skillList.RemoveAll(s => terroristSkills.Any(s2 => s2.Name == s.Skill.ToString()));
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag<jSkill_SkillInfo>? skills))
if (SkillUtils.IsPistolRound())
skillList.RemoveAll(s => SkillsInfo.GetValue<bool>(s.Skill, "disableOnPistolRound"));
if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out HashSet<Skills>? skills))
{
skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == s.Skill));
skillList.RemoveAll(s => skills.Contains(s.Skill));
if (skillList.Count == 0) skills.Clear();
}

View file

@ -8,11 +8,9 @@ namespace src.player.skills
public class Assassin : ISkill
{
private const Skills skillName = Skills.Assassin;
private static readonly string[] nadeWeapons =
[
"weapon_inferno", "weapon_molotov", "weapon_incgrenade", "weapon_flashbang",
"weapon_smokegrenade", "weapon_decoy", "weapon_hegrenade"
];
private const string infernoWeapon = "weapon_inferno";
private static bool IsNade(string? weapon) => weapon == infernoWeapon || WeaponPool.IsGrenade(weapon);
public static void LoadSkill()
{
@ -46,7 +44,7 @@ namespace src.player.skills
var weapon = param2.Ability?.Value;
if (weapon == null || !weapon.IsValid) return;
if (nadeWeapons.Contains(weapon.DesignerName)) return;
if (IsNade(weapon.DesignerName)) return;
if (IsBehind(attacker, victim))
param2.Damage *= SkillsInfo.GetValue<float>(skillName, "damageMultiplier");

View file

@ -0,0 +1,175 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
namespace src.player.skills
{
public class Bounty : ISkill
{
private const Skills skillName = Skills.Bounty;
private static readonly ConcurrentDictionary<uint, byte> bountyTargets = [];
private static readonly ConcurrentDictionary<uint, uint> ownerToTarget = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
bountyTargets.Clear();
ownerToTarget.Clear();
foreach (var player in PlayerManager.GetTickPlayers())
SkillUtils.CloseMenu(player);
}
public static void PlayerDisconnect(uint playerIndex)
{
bountyTargets.TryRemove(playerIndex, out _);
ownerToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in ownerToTarget)
if (kvp.Value == playerIndex)
ownerToTarget.TryRemove(kvp.Key, out _);
}
public static void EnableSkill(CCSPlayerController player)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
playerInfo.SkillUsed = false;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
var enemies = SkillUtils.GetSelectableEnemies(player, true);
if (enemies.Length > 0)
{
ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString())));
SkillUtils.CreateMenu(player, menuItems);
}
else
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
if (ownerToTarget.TryRemove(player.Index, out uint targetIndex))
bountyTargets.TryRemove(targetIndex, out _);
SkillUtils.CloseMenu(player);
}
public static void OnTick()
{
if (Server.TickCount % 32 != 0) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null || playerInfo.Skill != skillName) continue;
if (!SkillUtils.HasMenu(player)) continue;
var enemies = SkillUtils.GetSelectableEnemies(player, true);
ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString())));
SkillUtils.UpdateMenu(player, menuItems);
}
}
public static void TypeSkill(CCSPlayerController player, string[] commands)
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
if (playerInfo.SkillUsed)
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("areareaper_used_info")}");
return;
}
if (commands.Length == 0 || !uint.TryParse(commands[0], out uint enemyIndex))
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
return;
}
var enemy = Utilities.GetPlayerFromIndex((int)enemyIndex);
if (enemy == null || !enemy.IsValid)
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
return;
}
bountyTargets[enemy.Index] = 0;
ownerToTarget[player.Index] = enemy.Index;
playerInfo.SkillUsed = true;
int reward = SkillsInfo.GetValue<int>(skillName, "reward");
playerEvent.PrintToChat($" {ChatColors.Green}{playerEvent.GetTranslation("bounty_player_info", enemy.PlayerName, reward)}");
var enemyEvent = PlayerManager.GetPlayerFromEvent(enemy);
if (enemyEvent != null && enemyEvent.IsValid)
enemyEvent.PrintToChat($" {ChatColors.Red}{enemyEvent.GetTranslation("bounty_enemy_info", reward)}");
}
public static void PlayerDeath(EventPlayerDeath @event)
{
if (bountyTargets.IsEmpty) return;
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return;
if (!bountyTargets.TryRemove(victim.Index, out _)) return;
foreach (var kvp in ownerToTarget)
if (kvp.Value == victim.Index)
ownerToTarget.TryRemove(kvp.Key, out _);
var killer = PlayerManager.GetPlayerEvent(@event.Attacker);
if (killer == null || !killer.IsValid) return;
if (killer.Index == victim.Index) return;
if (killer.Team == victim.Team) return;
int reward = SkillsInfo.GetValue<int>(skillName, "reward");
if (!GiveMoney(killer, reward)) return;
var killerEvent = PlayerManager.GetPlayerFromEvent(killer);
if (killerEvent != null && killerEvent.IsValid)
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 bool GiveMoney(CCSPlayerController player, int amount)
{
var moneyServices = player.InGameMoneyServices;
if (moneyServices == null) return false;
int maxMoney = GetMaxMoney();
if (moneyServices.Account >= maxMoney) return false;
moneyServices.Account = Math.Clamp(moneyServices.Account + amount, 0, maxMoney);
Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices");
return true;
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#f2c14e", 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 reward = 300) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public int Reward { get; set; } = reward;
}
}
}

View file

@ -105,35 +105,27 @@ namespace src.player.skills
var bomb = PlayerManager.GetTickBomb();
var hidden = SkillUtils.ResolveHiddenPawns(invisiblePlayers.Keys, null);
if (hidden.Count == 0) return;
foreach (var (info, player) in infoList)
{
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero;
foreach (var playerIndex in invisiblePlayers.Keys)
foreach (var target in hidden)
{
var playerController = PlayerManager.GetPlayerEvent(Utilities.GetPlayerFromIndex((int)playerIndex));
if (playerController == null || !playerController.IsValid || playerController.Index == player.Index)
continue;
if (player.Team == playerController.Team)
continue;
var playerPawn = playerController.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid) continue;
if (target.Index == player.Index) continue;
if (player.Team == target.Team) continue;
// Only the actively spectated pawn stays transmitted; hiding it breaks the camera.
if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle)
continue;
if (targetHandle != nint.Zero && target.Pawn.Handle == targetHandle) continue;
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
if (entity == null || !entity.IsValid) continue;
if (info.TransmitEntities.Contains(target.Pawn.Index))
info.TransmitEntities.Remove(target.Pawn.Index);
if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index);
SkillUtils.HideCarriedEntities(info, playerPawn);
SkillUtils.HideCarriedEntities(info, target.Pawn);
if (bomb == null) continue;

View file

@ -12,6 +12,7 @@ namespace src.player.skills
public class CarefulBullets : ISkill
{
private const Skills skillName = Skills.CarefulBullets;
private const string hurtSound = "Player.DamageBody.Victim";
private static readonly ConcurrentDictionary<uint, byte> targetPlayers = [];
private static readonly ConcurrentDictionary<uint, bool> lastShot = [];
private static readonly ConcurrentDictionary<uint, int> hitPlayer = [];
@ -153,7 +154,7 @@ namespace src.player.skills
{
if (lastShot.TryRemove(playerIndex, out bool didHit) && !didHit)
{
eventPlayer.ExecuteClientCommand($"play player/player_damagebody_0{Instance.Random.Next(4, 8)}");
SkillUtils.EmitSoundToPlayer(eventPlayer, hurtSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
SkillUtils.TakeHealth(playerPawn, SkillsInfo.GetValue<int>(skillName, "damageAfterMiss"), GetSkillOwner(playerIndex), KillfeedIcons.Fist);
}
@ -257,8 +258,9 @@ namespace src.player.skills
targetToPlayer.TryRemove(index, out _);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#db6c35", 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 damageAfterMiss = 5) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#db6c35", 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 damageAfterMiss = 5, float soundVolume = .35f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int DamageAfterMiss { get; set; } = damageAfterMiss;
}
}

View file

@ -23,6 +23,8 @@ namespace src.player.skills
if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim) return;
var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index);
if (Heavyweight.Resists(victim)) return;
if (attackerInfo?.Skill == skillName && victim!.PawnIsAlive)
if (Instance.Random.NextDouble() <= attackerInfo.SkillChance)
{

View file

@ -10,16 +10,7 @@ namespace src.player.skills
public class Chicken : ISkill
{
private const Skills skillName = Skills.Chicken;
private static readonly HashSet<string> disabledWeapons =
[
"weapon_ak47", "weapon_m4a4", "weapon_m4a1", "weapon_m4a1_silencer",
"weapon_famas", "weapon_galilar", "weapon_aug", "weapon_sg556",
"weapon_mp9", "weapon_mac10", "weapon_bizon", "weapon_mp7",
"weapon_ump45", "weapon_p90", "weapon_mp5sd", "weapon_ssg08",
"weapon_awp", "weapon_scar20", "weapon_g3sg1", "weapon_nova",
"weapon_xm1014", "weapon_mag7", "weapon_sawedoff", "weapon_m249",
"weapon_negev"
];
private static HashSet<string> disabledWeapons => WeaponPool.Rifles;
private const string chickenModelPath = "models/chicken/chicken.vmdl";
private static readonly ConcurrentDictionary<uint, uint> chickens = [];

View file

@ -88,7 +88,7 @@ namespace src.player.skills
{
var cameraProp = Utilities.GetEntityFromIndex<CDynamicProp>((int)playerSkill.CameraProp);
if (cameraProp != null && cameraProp.IsValid)
cameraProp.EmitSound("SolidMetal.BulletImpact");
cameraProp.EmitSound("SolidMetal.BulletImpact", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
SkillUtils.SafeKillEntity<CDynamicProp>(playerSkill.CameraProp);
playerSkill.CameraProp = null;
@ -414,8 +414,9 @@ namespace src.player.skills
public required QAngle LastCameraAngle { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#34ebd5", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float cooldown = 30) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#34ebd5", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float cooldown = 30, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
}
}

View file

@ -143,6 +143,8 @@ namespace src.player.skills
eventPlayerPawn.AbsVelocity.X = newVelocity.X;
eventPlayerPawn.AbsVelocity.Y = newVelocity.Y;
eventPlayerPawn.AbsVelocity.Z = newVelocity.Z;
eventPlayerPawn.EmitSound("Default.WalkJump", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
@ -164,8 +166,9 @@ namespace src.player.skills
public int JumpReleasedTicks { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#42bbfc", 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, float jumpVelocity = 150f, float pushVelocity = 600f, bool anyDirection = true, float cooldown = 2f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#42bbfc", 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, float jumpVelocity = 150f, float pushVelocity = 600f, bool anyDirection = true, float cooldown = 2f, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float JumpVelocity { get; set; } = jumpVelocity;
public float PushVelocity { get; set; } = pushVelocity;
public bool AnyDirection { get; set; } = anyDirection;

View file

@ -13,6 +13,7 @@ namespace src.player.skills
public class DeathBomb : ISkill
{
private const Skills skillName = Skills.DeathBomb;
private static readonly string[] voiceLines = ["balkan.radiobotfallback01", "balkan.radiobotfallback02", "balkan.radiobotfallback04"];
private static readonly QAngle angle = new(10, -5, 9);
private static readonly ConcurrentDictionary<int, (byte Team, uint Owner)> nades = [];
@ -61,9 +62,7 @@ namespace src.player.skills
SkillUtils.PrintToChat(_p, $"{ChatColors.DarkRed}\u202A{player.PlayerName}\u202C: {ChatColors.Lime}{_p.GetTranslation("deathbomb_explosion")}",
border: !PlayerManager.GetTickPlayers().Any(p => p.Team == player.Team && p != player) ? "tb" : "t");
var fileNames = new[] { "radiobotfallback01", "radiobotfallback02", "radiobotfallback04" };
var randomFile = fileNames[new Random().Next(fileNames.Length)];
player.ExecuteClientCommand($"play vo/agents/balkan/{randomFile}.vsnd");
player.EmitSound(voiceLines[Random.Shared.Next(voiceLines.Length)], volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
nades.AddOrUpdate(Server.TickCount, (player.TeamNum, player.Index), (_, _) => (player.TeamNum, player.Index));
}
@ -136,8 +135,9 @@ namespace src.player.skills
return player != null && player.IsValid && player.PlayerPawn?.Value != null;
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#F5CB42", 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, float explosionRadius = 500.0f, int explosionDamage = 999, float dmgReductionForTeamates = 0.5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#F5CB42", 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, float explosionRadius = 500.0f, int explosionDamage = 999, float dmgReductionForTeamates = 0.5f, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float ExplosionRadius { get; set; } = explosionRadius;
public int ExplosionDamage { get; set; } = explosionDamage;
public float DmgReductionForTeamates { get; set; } = dmgReductionForTeamates;

View file

@ -66,13 +66,14 @@ namespace src.player.skills
if (playerSeesEnemy)
{
SkillUtils.TakeHealth(enemyPawn, damage, player, KillfeedIcons.Fist);
enemyEvent.EmitSound("Player.DamageBody.Onlooker");
SkillUtils.EmitSoundToPlayer(enemyEvent, "Player.DamageBody.Victim", SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#c91243", 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, float secondCooldown = 1f, int damage = 5) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#c91243", 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, float secondCooldown = 1f, int damage = 5, float soundVolume = .3f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float SecondCooldown { get; set; } = secondCooldown;
public int Damage { get; set; } = damage;
}

View file

@ -0,0 +1,154 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
namespace src.player.skills
{
public class EmpGrenade : ISkill
{
private const Skills skillName = Skills.EmpGrenade;
private const string ownerTag = "EmpGrenade";
private static readonly ConcurrentDictionary<uint, int> jammedUntilTick = [];
private static readonly ConcurrentDictionary<uint, int> playersWithSkill = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
RestoreAll();
jammedUntilTick.Clear();
}
public static void RoundEnd()
{
RestoreAll();
jammedUntilTick.Clear();
}
public static void PlayerDisconnect(uint playerIndex)
{
jammedUntilTick.TryRemove(playerIndex, out _);
playersWithSkill.TryRemove(playerIndex, out _);
}
public static void PlayerDeath(EventPlayerDeath @event)
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return;
if (jammedUntilTick.TryRemove(victim.Index, out _))
SetHud(victim.Index, false);
}
public static void PlayerHurt(EventPlayerHurt @event)
{
if (@event.Weapon != "hegrenade") return;
var attacker = PlayerManager.GetPlayerEvent(@event.Attacker);
if (attacker == null || !attacker.IsValid) return;
var attackerInfo = PlayerManager.GetPlayerByIndex(attacker.Index);
if (attackerInfo?.Skill != skillName) return;
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return;
float duration = SkillsInfo.GetValue<float>(skillName, "jamSeconds");
jammedUntilTick[victim.Index] = Server.TickCount + (int)(duration * 64);
SetHud(victim.Index, true);
var victimEvent = PlayerManager.GetPlayerFromEvent(victim);
if (victimEvent != null && victimEvent.IsValid)
victimEvent.PrintToChat($" {ChatColors.Red}{victimEvent.GetTranslation("empgrenade_enemy_info")}");
}
public static void OnTick()
{
if (jammedUntilTick.IsEmpty) return;
int now = Server.TickCount;
foreach (var kvp in jammedUntilTick)
{
if (now < kvp.Value) continue;
if (!jammedUntilTick.TryRemove(kvp.Key, out _)) continue;
SetHud(kvp.Key, false);
}
}
private static void RestoreAll()
{
foreach (var playerIndex in jammedUntilTick.Keys)
SetHud(playerIndex, false);
}
private static void SetHud(uint playerIndex, bool jam)
{
var player = Utilities.GetPlayerFromIndex((int)playerIndex);
if (player == null || !player.IsValid) return;
ApplyHud(player, jam);
ApplyHud(PlayerManager.GetPlayerFromEvent(player), jam);
}
private static void ApplyHud(CCSPlayerController? player, bool jam)
{
if (player == null || !player.IsValid) return;
SkillUtils.SetCrosshairHidden(player, ownerTag, jam);
SkillUtils.SetRadarDisabled(player, ownerTag, jam);
}
public static void GrenadeThrown(EventGrenadeThrown @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
if (@event.Weapon != "hegrenade") return;
var playerInfo = PlayerManager.GetPlayerByIndex(player.Index);
if (playerInfo?.Skill != skillName) return;
if (playersWithSkill.TryGetValue(player.Index, out int grenadesLeft) && grenadesLeft > 1)
{
playersWithSkill[player.Index] = grenadesLeft - 1;
player!.GiveNamedItem("weapon_hegrenade");
SkillUtils.UpdateGrenadeCount(player, CsItem.HEGrenade, grenadesLeft - 1);
}
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
int grenadeLimit = SkillsInfo.GetValue<int>(skillName, "grenadeLimit");
playersWithSkill[player.Index] = grenadeLimit;
SkillUtils.TryGiveWeapon(player, CsItem.HEGrenade);
SkillUtils.UpdateGrenadeCount(player, CsItem.HEGrenade, grenadeLimit);
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
playersWithSkill.TryRemove(player.Index, out _);
SkillUtils.UpdateGrenadeCount(player, CsItem.HEGrenade, 1);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#3ad6e8", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Uncommon, float jamSeconds = 3f, int grenadeLimit = 2) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float JamSeconds { get; set; } = jamSeconds;
public int GrenadeLimit { get; set; } = grenadeLimit;
}
}
}

View file

@ -19,12 +19,14 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
DecoyRing.PreloadAssets();
}
public static void NewRound()
{
KillAllDecoys();
rainMolotovs.Clear();
DecoyRing.ClearAll(skillName);
}
private static void KillAllDecoys()
@ -276,7 +278,14 @@ namespace src.player.skills
if (decoy != null && decoy.IsValid)
decoy.AddEntityIOEvent("Kill", decoy, delay: 0.1f);
decoys.TryRemove(key, out _);
SpawnRain(player, new Vector(@event.X, @event.Y, @event.Z));
Vector impact = new(@event.X, @event.Y, @event.Z);
SpawnRain(player, impact);
DecoyRing.Show(skillName, key, impact, 130f);
jRandomSkills.Instance.AddTimer(SkillsInfo.GetValue<float>(skillName, "markerSeconds"),
() => DecoyRing.Hide(skillName, key),
CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
}
@ -286,8 +295,9 @@ namespace src.player.skills
SkillUtils.TryGiveWeapon(player, CsItem.DecoyGrenade);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ffbf47", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Epic) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ffbf47", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Epic, float markerSeconds = 4f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float MarkerSeconds { get; set; } = markerSeconds;
}
}
}

View file

@ -154,7 +154,7 @@ namespace src.player.skills
var box = param.As<CDynamicProp>();
if (box == null || !box.IsValid) return;
box.EmitSound("Wood_Plank.BulletImpact", volume: 1f);
box.EmitSound("Wood_Plank.BulletImpact", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
if (barricades.TryGetValue(box.Index, out int health))
{
@ -178,8 +178,9 @@ namespace src.player.skills
public DateTime Cooldown { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#1b04cc", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 5, Rarity rarity = Rarity.Common, float cooldown = 2f, int barricadeHealth = 115, string propModel = "models/props/de_aztec/hr_aztec/aztec_scaffolding/aztec_scaffold_wall_support_128.vmdl") : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#1b04cc", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 5, Rarity rarity = Rarity.Common, float cooldown = 2f, int barricadeHealth = 115, string propModel = "models/props/de_aztec/hr_aztec/aztec_scaffolding/aztec_scaffold_wall_support_128.vmdl", float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
public int BarricadeHealth { get; set; } = barricadeHealth;
public string PropModel { get; set; } = propModel;

View file

@ -16,11 +16,19 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
DecoyRing.PreloadAssets();
}
public static void NewRound()
{
decoys.Clear();
DecoyRing.ClearAll(skillName);
}
public static void RoundEnd()
{
decoys.Clear();
DecoyRing.ClearAll(skillName);
}
public static void DecoyStarted(EventDecoyStarted @event)
@ -31,7 +39,9 @@ namespace src.player.skills
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
decoys.TryAdd(new Vector(@event.X, @event.Y, @event.Z), 0);
Vector pos = new(@event.X, @event.Y, @event.Z);
decoys.TryAdd(pos, 0);
DecoyRing.Show(skillName, (uint)@event.Entityid, pos, SkillsInfo.GetValue<float>(skillName, "triggerRadius"));
}
public static void DecoyDetonate(EventDecoyDetonate @event)
@ -44,6 +54,8 @@ namespace src.player.skills
foreach (var decoy in decoys.Keys.Where(v => v.X == @event.X && v.Y == @event.Y && v.Z == @event.Z))
decoys.TryRemove(decoy, out _);
DecoyRing.Hide(skillName, (uint)@event.Entityid);
}
public static void OnTick()

View file

@ -13,9 +13,13 @@ namespace src.player.skills
public class Ghost : ISkill
{
private const Skills skillName = Skills.Ghost;
private static readonly string[] allowedWeapons = [
"weapon_molotov", "weapon_incgrenade", "weapon_flashbang", "weapon_smokegrenade", "weapon_decoy", "weapon_hegrenade", "weapon_knife", "weapon_bayonet", "weapon_c4"
];
private static readonly HashSet<string> alwaysAllowed = new(StringComparer.Ordinal)
{
"weapon_knife", "weapon_bayonet", "weapon_c4"
};
private static bool IsAllowed(string? weapon) =>
weapon != null && (alwaysAllowed.Contains(weapon) || WeaponPool.IsGrenade(weapon));
private static readonly ConcurrentDictionary<uint, byte> invisiblePlayers = [];
private const string bloodParticle = "particles/blood_impact/blood_impact_high.vpcf";
private const string cameraViewModel = "models/sprays/spray_plane.vmdl";
@ -61,38 +65,30 @@ namespace src.player.skills
var bomb = PlayerManager.GetTickBomb();
uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null;
var hidden = SkillUtils.ResolveHiddenPawns(invisiblePlayers.Keys, bombOwnerIndex);
if (hidden.Count == 0) return;
foreach (var (info, player) in infoList)
{
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero;
foreach (var playerIndex in invisiblePlayers.Keys)
foreach (var target in hidden)
{
var playerController = Utilities.GetPlayerFromIndex((int)playerIndex);
if (playerController == null || !playerController.IsValid || playerController.Index == player.Index)
continue;
if (player.Team == playerController.Team)
continue;
var playerPawn = playerController.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid) continue;
if (target.Index == player.Index) continue;
if (player.Team == target.Team) continue;
// Only the actively spectated pawn stays transmitted; hiding it breaks the camera.
if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle)
continue;
if (targetHandle != nint.Zero && target.Pawn.Handle == targetHandle) continue;
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
if (entity == null || !entity.IsValid) continue;
if (info.TransmitEntities.Contains(target.Pawn.Index))
info.TransmitEntities.Remove(target.Pawn.Index);
if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index);
SkillUtils.HideCarriedEntities(info, playerPawn);
SkillUtils.HideCarriedEntities(info, target.Pawn);
// Hide the bomb as well, but only while this hidden player is the one holding it.
if (bomb == null || bombOwnerIndex != playerController.Index) continue;
if (bomb == null || !target.HoldsBomb) continue;
if (info.TransmitEntities.Contains(bomb.Index))
info.TransmitEntities.Remove(bomb.Index);
@ -156,7 +152,7 @@ namespace src.player.skills
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill == skillName)
if (playerInfo?.Skill == skillName && SkillUtils.IsHudFrame())
UpdateHUD(player);
if (player.LifeState != (byte)LifeState_t.LIFE_ALIVE)
@ -206,7 +202,7 @@ namespace src.player.skills
if (weapon != null && weapon.IsValid && weapon.Value != null && weapon.Value.IsValid && !string.IsNullOrEmpty(weapon.Value.DesignerName))
{
string weaponName = weapon.Value.DesignerName;
if (!allowedWeapons.Contains(weaponName))
if (!IsAllowed(weaponName))
{
weapon.Value.NextPrimaryAttackTick = disableWeapon ? int.MaxValue : Server.TickCount;
weapon.Value.NextSecondaryAttackTick = disableWeapon ? int.MaxValue : Server.TickCount;
@ -227,7 +223,7 @@ namespace src.player.skills
if (playerInfo == null) return;
var weapon = pawn.WeaponServices.ActiveWeapon.Value;
if (weapon == null || !weapon.IsValid || allowedWeapons.Contains(weapon.DesignerName))
if (weapon == null || !weapon.IsValid || IsAllowed(weapon.DesignerName))
{
playerInfo.PrintHTML = null;
return;

View file

@ -41,6 +41,18 @@ namespace src.player.skills
{
if (smokes.IsEmpty || playersWithSkill.IsEmpty) return;
List<uint> smokeIndexes = new(smokes.Count);
foreach (var entityIndex in smokes.Keys)
{
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)entityIndex);
if (entity == null || !entity.IsValid) continue;
smokeIndexes.Add(entity.Index);
}
if (smokeIndexes.Count == 0) return;
Dictionary<nint, uint>? pawnOwners = null;
foreach (var (info, player) in infoList)
{
if (player == null || !player.IsValid) continue;
@ -51,23 +63,36 @@ namespace src.player.skills
var observerTarget = player.Pawn?.Value?.ObserverServices?.ObserverTarget?.Value?.Handle ?? nint.Zero;
if (observerTarget == nint.Zero) continue;
var observedPlayer = PlayerManager.GetTickPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == observerTarget);
if (observedPlayer == null) continue;
pawnOwners ??= BuildPawnOwners();
if (PlayerManager.GetPlayerByIndex(observedPlayer.Index)?.Skill != skillName) continue;
if (!pawnOwners.TryGetValue(observerTarget, out uint observedIndex)) continue;
if (PlayerManager.GetPlayerByIndex(observedIndex)?.Skill != skillName) continue;
}
foreach (var entityIndex in smokes.Keys)
{
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)entityIndex);
if (entity == null || !entity.IsValid) continue;
if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index);
}
foreach (var index in smokeIndexes)
if (info.TransmitEntities.Contains(index))
info.TransmitEntities.Remove(index);
}
}
private static Dictionary<nint, uint> BuildPawnOwners()
{
var players = PlayerManager.GetTickPlayers();
Dictionary<nint, uint> owners = new(players.Count);
foreach (var p in players)
{
if (p == null || !p.IsValid) continue;
nint handle = p.Pawn?.Value?.Handle ?? nint.Zero;
if (handle == nint.Zero) continue;
owners.TryAdd(handle, p.Index);
}
return owners;
}
public static void GrenadeThrown(EventGrenadeThrown @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);

View file

@ -9,6 +9,7 @@ namespace src.player.skills
public class Glitch : ISkill
{
private const Skills skillName = Skills.Glitch;
private const string ownerTag = "Glitch";
private static readonly ConcurrentDictionary<uint, byte> glitchedPlayers = [];
private static readonly ConcurrentDictionary<uint, uint> playersToTarget = [];
private static readonly object setLock = new();
@ -100,7 +101,7 @@ namespace src.player.skills
var enemyEvent = PlayerManager.GetPlayerFromEvent(enemy);
if (enemyEvent == null || !enemyEvent.IsValid) return;
enemyEvent.ReplicateConVar("sv_disable_radar", "1");
SkillUtils.SetRadarDisabled(enemyEvent, ownerTag, true);
playerInfo.SkillUsed = true;
playerEvent.PrintToChat($" {ChatColors.Green}" + playerEvent.GetTranslation("glitch_player_info", enemy.PlayerName));
@ -117,7 +118,7 @@ namespace src.player.skills
if (!glitchedPlayers.ContainsKey(bot.Index)) return;
player.ReplicateConVar("sv_disable_radar", "1");
SkillUtils.SetRadarDisabled(player, ownerTag, true);
}
public static void EnableSkill(CCSPlayerController player)
@ -146,7 +147,7 @@ namespace src.player.skills
var target = PlayerManager.GetPlayerFromEvent(Utilities.GetPlayerFromIndex((int)targetIndex));
if (target != null && target.IsValid)
{
target.ReplicateConVar("sv_disable_radar", "0");
SkillUtils.SetRadarDisabled(target, ownerTag, false);
if (target.PawnIsAlive && !SkillUtils.IsFreezeTime())
target.PrintToChat($" {ChatColors.Green}" + target.GetTranslation("glitch_disable_info"));
}
@ -161,7 +162,7 @@ namespace src.player.skills
var player = @event.Userid;
if (player == null || !player.IsValid) return;
player.ReplicateConVar("sv_disable_radar", "0");
SkillUtils.SetRadarDisabled(player, ownerTag, false);
glitchedPlayers.TryRemove(player.Index, out _);
SkillUtils.CloseMenu(player);
}

View file

@ -20,10 +20,13 @@ namespace src.player.skills
private static readonly Color counterTerroristRope = Color.FromArgb(255, 52, 92, 138);
private const double attemptCooldown = .3;
private const string hookModel = "models/generic/grapplinghook_01/grapplinghook_hook_01_open.vmdl";
private const string hookSound = "SolidMetal.BulletImpact";
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
jRandomSkills.Instance.AddToManifest(hookModel);
}
public static void NewRound()
@ -131,6 +134,8 @@ namespace src.player.skills
skillInfo.Anchor = anchor;
skillInfo.EndTick = Server.TickCount + (int)(SkillsInfo.GetValue<float>(skillName, "maxPullSeconds") * 64);
SpawnHook(player, skillInfo, anchor, result.Value.NormalX, result.Value.NormalY, result.Value.NormalZ);
if (!EntityManager.OverBudget())
{
Color ropeColor = player.Team == CsTeam.Terrorist ? terroristRope : counterTerroristRope;
@ -155,15 +160,102 @@ namespace src.player.skills
}
}
player.EmitSound("SolidMetal.BulletImpact");
return true;
}
private static void EmitHookSound(CBaseEntity? source, CCSPlayerController player)
{
float volume = SkillsInfo.GetValue<float>(skillName, "soundVolume");
if (source != null && source.IsValid)
{
source.EmitSound(hookSound, volume: volume);
return;
}
var pawn = player.PlayerPawn?.Value;
if (pawn != null && pawn.IsValid)
pawn.EmitSound(hookSound, volume: volume);
}
private static void SpawnHook(CCSPlayerController player, PlayerSkillInfo skillInfo, Vector anchor, float normalX, float normalY, float normalZ)
{
float length = MathF.Sqrt(normalX * normalX + normalY * normalY + normalZ * normalZ);
if (length < .0001f)
{
EmitHookSound(null, player);
return;
}
normalX /= length;
normalY /= length;
normalZ /= length;
var hook = EntityManager.CreateTrackedPropOverride(player.Index);
if (hook == null || !hook.IsValid)
{
EmitHookSound(null, player);
return;
}
float pitch = -MathF.Asin(Math.Clamp(-normalZ, -1f, 1f)) * 180f / MathF.PI;
float yaw = MathF.Atan2(-normalY, -normalX) * 180f / MathF.PI;
QAngle angle = new(pitch, yaw, 0);
float scale = SkillsInfo.GetValue<float>(skillName, "hookScale");
float embed = SkillsInfo.GetValue<float>(skillName, "hookEmbed") * scale;
Vector pos = new(anchor.X + normalX * embed, anchor.Y + normalY * embed, anchor.Z + normalZ * embed);
uint hookIndex = hook.Index;
skillInfo.HookIndex = hookIndex;
Server.NextFrame(() =>
{
var prop = Utilities.GetEntityFromIndex<CDynamicProp>((int)hookIndex);
if (prop == null || !prop.IsValid)
{
EmitHookSound(null, player);
return;
}
var node = prop.CBodyComponent?.SceneNode?.Owner?.Entity;
if (node != null)
node.Flags = (uint)(node.Flags & ~(1 << 2));
prop.SetModel(hookModel);
prop.Teleport(pos, angle);
prop.DispatchSpawn();
EmitHookSound(prop, player);
var skeleton = prop.CBodyComponent?.SceneNode?.GetSkeletonInstance();
if (skeleton != null)
{
skeleton.Scale = scale;
prop.AcceptInput("SetScale", null, null, scale.ToString(System.Globalization.CultureInfo.InvariantCulture));
}
Utilities.SetStateChanged(prop, "CBaseEntity", "m_CBodyComponent");
});
}
private static void DestroyHook(PlayerSkillInfo skillInfo)
{
if (skillInfo.HookIndex == null) return;
uint hookIndex = skillInfo.HookIndex.Value;
skillInfo.HookIndex = null;
EntityManager.DestroyEntity(hookIndex);
}
private static void DestroyRope(PlayerSkillInfo skillInfo)
{
skillInfo.Anchor = null;
skillInfo.EndTick = 0;
DestroyHook(skillInfo);
if (skillInfo.RopeIndex == null) return;
uint ropeIndex = skillInfo.RopeIndex.Value;
@ -176,26 +268,7 @@ namespace src.player.skills
{
if (!liveRopes.TryRemove(ropeIndex, out _)) return;
var rope = Utilities.GetEntityFromIndex<CBeam>((int)ropeIndex);
if (rope != null && rope.IsValid)
{
rope.Width = 0;
rope.EndWidth = 0;
rope.Render = Color.FromArgb(0, 0, 0, 0);
Utilities.SetStateChanged(rope, "CBeam", "m_fWidth");
Utilities.SetStateChanged(rope, "CBeam", "m_fEndWidth");
Utilities.SetStateChanged(rope, "CBaseModelEntity", "m_clrRender");
}
EntityManager.DestroyEntity(ropeIndex);
Server.NextFrame(() =>
{
var leftover = Utilities.GetEntityFromIndex<CBeam>((int)ropeIndex);
if (leftover != null && leftover.IsValid)
leftover.Remove();
});
EntityManager.DestroyBeam(ropeIndex);
}
private static void SweepOrphanRopes()
@ -299,11 +372,13 @@ namespace src.player.skills
public DateTime LastAttempt { get; set; }
public Vector? Anchor { get; set; }
public uint? RopeIndex { get; set; }
public uint? HookIndex { get; set; }
public int EndTick { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#38e0c4", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, float cooldown = 10f, float maxDistance = 1500f, float minDistance = 150f, float stopDistance = 90f, float pullSpeed = 850f, float maxPullSeconds = 3f, float ropeWidth = 0.8f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#38e0c4", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, float cooldown = 10f, float maxDistance = 1500f, float minDistance = 150f, float stopDistance = 90f, float pullSpeed = 850f, float maxPullSeconds = 3f, float ropeWidth = 0.8f, float hookEmbed = 8f, float hookScale = .4f, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
public float MaxDistance { get; set; } = maxDistance;
public float MinDistance { get; set; } = minDistance;
@ -311,6 +386,8 @@ namespace src.player.skills
public float PullSpeed { get; set; } = pullSpeed;
public float MaxPullSeconds { get; set; } = maxPullSeconds;
public float RopeWidth { get; set; } = ropeWidth;
public float HookEmbed { get; set; } = hookEmbed;
public float HookScale { get; set; } = hookScale;
}
}
}

View file

@ -0,0 +1,189 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
namespace src.player.skills
{
public class GravityDecoy : ISkill
{
private const Skills skillName = Skills.GravityDecoy;
private const float defaultGravity = 1f;
private static readonly ConcurrentDictionary<Vector, byte> decoys = [];
private static readonly ConcurrentDictionary<uint, int> playersWithSkill = [];
private static readonly ConcurrentDictionary<uint, byte> affected = [];
private static readonly ConcurrentDictionary<uint, byte> restoreOnRespawn = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
DecoyRing.PreloadAssets();
}
public static void NewRound()
{
RestoreAll();
decoys.Clear();
DecoyRing.ClearAll(skillName);
}
public static void RoundEnd()
{
RestoreAll();
decoys.Clear();
DecoyRing.ClearAll(skillName);
}
public static void DecoyStarted(EventDecoyStarted @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
Vector pos = new(@event.X, @event.Y, @event.Z);
decoys.TryAdd(pos, 0);
DecoyRing.Show(skillName, (uint)@event.Entityid, pos, SkillsInfo.GetValue<float>(skillName, "triggerRadius"));
}
public static void DecoyDetonate(EventDecoyDetonate @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
foreach (var decoy in decoys.Keys.Where(v => v.X == @event.X && v.Y == @event.Y && v.Z == @event.Z))
decoys.TryRemove(decoy, out _);
DecoyRing.Hide(skillName, (uint)@event.Entityid);
if (decoys.IsEmpty) RestoreAll();
}
public static void OnTick()
{
if (decoys.IsEmpty && affected.IsEmpty && restoreOnRespawn.IsEmpty) return;
if (decoys.IsEmpty && !affected.IsEmpty) RestoreAll();
float radius = SkillsInfo.GetValue<float>(skillName, "triggerRadius");
float gravity = SkillsInfo.GetValue<float>(skillName, "gravityScale");
foreach (var player in PlayerManager.GetTickPlayers())
{
if (player == null || !player.IsValid) continue;
if (player.Team is not (CsTeam.CounterTerrorist or CsTeam.Terrorist)) continue;
var eventPlayer = PlayerManager.GetPlayerEvent(player);
if (eventPlayer == null || !eventPlayer.IsValid) continue;
var pawn = eventPlayer.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null) continue;
if (pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE) continue;
if (restoreOnRespawn.TryRemove(eventPlayer.Index, out _))
pawn.ActualGravityScale = defaultGravity;
bool inside = !decoys.IsEmpty && decoys.Keys.Any(d => SkillUtils.GetDistance(d, pawn.AbsOrigin) <= radius);
if (inside)
{
if (!affected.TryAdd(eventPlayer.Index, 0)) continue;
pawn.ActualGravityScale = gravity;
}
else if (affected.TryRemove(eventPlayer.Index, out _))
pawn.ActualGravityScale = defaultGravity;
}
}
public static void PlayerDeath(EventPlayerDeath @event)
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return;
if (!affected.TryRemove(victim.Index, out _)) return;
Restore(victim.Index);
restoreOnRespawn[victim.Index] = 0;
}
public static void PlayerDisconnect(uint playerIndex)
{
affected.TryRemove(playerIndex, out _);
restoreOnRespawn.TryRemove(playerIndex, out _);
playersWithSkill.TryRemove(playerIndex, out _);
}
private static void RestoreAll()
{
foreach (uint playerIndex in affected.Keys)
Restore(playerIndex);
foreach (uint playerIndex in restoreOnRespawn.Keys)
Restore(playerIndex);
affected.Clear();
restoreOnRespawn.Clear();
}
private static void Restore(uint playerIndex)
{
var player = Utilities.GetPlayerFromIndex((int)playerIndex);
var pawn = player?.PlayerPawn?.Value;
if (pawn != null && pawn.IsValid)
pawn.ActualGravityScale = defaultGravity;
}
public static void GrenadeThrown(EventGrenadeThrown @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
if (@event.Weapon != "decoy") return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
if (playersWithSkill.TryGetValue(player.Index, out int grenadesLeft) && grenadesLeft > 1)
{
playersWithSkill[player.Index] = grenadesLeft - 1;
player!.GiveNamedItem("weapon_decoy");
SkillUtils.UpdateGrenadeCount(player, CsItem.DecoyGrenade, grenadesLeft - 1);
}
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
int grenadeLimit = SkillsInfo.GetValue<int>(skillName, "grenadeLimit");
playersWithSkill[player.Index] = grenadeLimit;
SkillUtils.TryGiveWeapon(player, CsItem.DecoyGrenade);
SkillUtils.UpdateGrenadeCount(player, CsItem.DecoyGrenade, grenadeLimit);
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
playersWithSkill.TryRemove(player.Index, out _);
SkillUtils.UpdateGrenadeCount(player, CsItem.DecoyGrenade, 1);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#a06cd5", 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, float triggerRadius = 180f, float gravityScale = .5f, int grenadeLimit = 2) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float TriggerRadius { get; set; } = triggerRadius;
public float GravityScale { get; set; } = gravityScale;
public int GrenadeLimit { get; set; } = grenadeLimit;
}
}
}

View file

@ -92,7 +92,7 @@ namespace src.player.skills
if (SkillUtils.GetDistance(smokePos, pawn.AbsOrigin) <= smokeRadius)
if (SkillUtils.AddHealth(pawn, smokeHeal))
player.EmitSound("Healthshot.Success");
SkillUtils.EmitSoundToPlayer(player, "Healthshot.Success", SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
@ -156,8 +156,9 @@ namespace src.player.skills
SkillUtils.UpdateGrenadeCount(player, CsItem.SmokeGrenade, 1);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#1fe070", 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 smokeHeal = 1, float smokeRadius = 180, int tickCooldown = 16, int grenadeLimit = 1) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#1fe070", 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 smokeHeal = 1, float smokeRadius = 180, int tickCooldown = 16, int grenadeLimit = 1, float soundVolume = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int SmokeHeal { get; set; } = smokeHeal;
public float SmokeRadius { get; set; } = smokeRadius;
public int TickCooldown { get; set; } = tickCooldown;

View file

@ -0,0 +1,55 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
namespace src.player.skills
{
public class Heavyweight : ISkill
{
private const Skills skillName = Skills.Heavyweight;
private static readonly ConcurrentDictionary<uint, byte> holders = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
holders.Clear();
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
holders[player.Index] = 0;
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
holders.TryRemove(player.Index, out _);
}
public static void PlayerDisconnect(uint playerIndex)
{
holders.TryRemove(playerIndex, out _);
}
public static bool Resists(CCSPlayerController? player)
{
if (player == null || !player.IsValid || holders.IsEmpty) return false;
if (holders.ContainsKey(player.Index)) return true;
uint routedIndex = PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index;
return routedIndex != player.Index && holders.ContainsKey(routedIndex);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#8d6e63", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Uncommon) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
}
}
}

View file

@ -10,19 +10,7 @@ namespace src.player.skills
public class Hermit : ISkill
{
private const Skills skillName = Skills.Hermit;
private static readonly ConcurrentDictionary<string, int> ConcurrentDictionary = new(
[
new KeyValuePair<string, int>("weapon_glock", 3), new KeyValuePair<string, int>("weapon_usp_silencer", 2), new KeyValuePair<string, int>("weapon_hkp2000", 4), new KeyValuePair<string, int>("weapon_p250", 3),
new KeyValuePair<string, int>("weapon_cz75", 2), new KeyValuePair<string, int>("weapon_deagle", 3), new KeyValuePair<string, int>("weapon_fiveseven", 2), new KeyValuePair<string, int>("weapon_elite", 2),
new KeyValuePair<string, int>("weapon_tec9", 3), new KeyValuePair<string, int>("weapon_revolver", 2), new KeyValuePair<string, int>("weapon_mac10", 3), new KeyValuePair<string, int>("weapon_mp9", 2),
new KeyValuePair<string, int>("weapon_mp7", 3), new KeyValuePair<string, int>("weapon_mp5", 3), new KeyValuePair<string, int>("weapon_mp5sd", 3), new KeyValuePair<string, int>("weapon_ump45", 3),
new KeyValuePair<string, int>("weapon_p90", 2), new KeyValuePair<string, int>("weapon_bizon", 2), new KeyValuePair<string, int>("weapon_ak47", 3), new KeyValuePair<string, int>("weapon_m4a1", 4),
new KeyValuePair<string, int>("weapon_m4a1_silencer", 3), new KeyValuePair<string, int>("weapon_galilar", 4), new KeyValuePair<string, int>("weapon_famas", 4), new KeyValuePair<string, int>("weapon_aug", 3),
new KeyValuePair<string, int>("weapon_sg556", 3), new KeyValuePair<string, int>("weapon_ssg08", 2), new KeyValuePair<string, int>("weapon_awp", 2), new KeyValuePair<string, int>("weapon_scar20", 2),
new KeyValuePair<string, int>("weapon_g3sg1", 2), new KeyValuePair<string, int>("weapon_nova", 32), new KeyValuePair<string, int>("weapon_xm1014", 32), new KeyValuePair<string, int>("weapon_sawedoff", 32),
new KeyValuePair<string, int>("weapon_mag7", 3), new KeyValuePair<string, int>("weapon_m249", 2), new KeyValuePair<string, int>("weapon_negev", 2)
]);
private static readonly ConcurrentDictionary<string, int> maxReserveAmmo = ConcurrentDictionary;
private const int fallbackReserveAmmo = 100;
public static void LoadSkill()
{
@ -43,9 +31,11 @@ namespace src.player.skills
var weapon = pawn.WeaponServices.ActiveWeapon.Value;
if (weapon == null || !weapon.IsValid || weapon.VData == null) return;
var maxReserveAmmoClip = maxReserveAmmo.TryGetValue(weapon.DesignerName, out var reserve) ? reserve : 100;
int reserve = weapon.As<CCSWeaponBase>().GetVData<CCSWeaponBaseVData>()?.PrimaryReserveAmmoMax ?? 0;
if (reserve <= 0) reserve = fallbackReserveAmmo;
weapon.Clip1 = weapon.VData.MaxClip1;
weapon.ReserveAmmo.Fill(maxReserveAmmoClip);
weapon.ReserveAmmo.Fill(reserve);
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_iClip1");
Utilities.SetStateChanged(weapon, "CBasePlayerWeapon", "m_pReserveAmmo");

View file

@ -12,6 +12,7 @@ namespace src.player.skills
public class HotBomb : ISkill
{
private const Skills skillName = Skills.HotBomb;
private const string hurtSound = "Player.DamageBody.Victim";
private readonly static ConcurrentDictionary<uint, byte> players = [];
public static void LoadSkill()
@ -41,7 +42,7 @@ namespace src.player.skills
if (!hasC4) continue;
SkillUtils.TakeHealth(pawn, damage, owner, KillfeedIcons.C4);
playerEvent?.ExecuteClientCommand($"play player/player_damagebody_0{jRandomSkills.Instance.Random.Next(4, 8)}");
SkillUtils.EmitSoundToPlayer(playerEvent, hurtSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
@ -121,8 +122,9 @@ namespace src.player.skills
}
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#baf081", CsTeam onlyTeam = CsTeam.CounterTerrorist, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 1, Rarity rarity = Rarity.Common, float cooldown = 1, int damage = 2) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#baf081", CsTeam onlyTeam = CsTeam.CounterTerrorist, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 1, Rarity rarity = Rarity.Common, float cooldown = 1, int damage = 2, float soundVolume = .35f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
public int Damage { get; set; } = damage;
}

View file

@ -112,7 +112,7 @@ namespace src.player.skills
var player = Utilities.GetPlayerFromIndex((int)playerSkill.PlayerIndex);
if (player == null || !player.IsValid) return;
player.EmitSound("SolidMetal.BulletImpact");
player.EmitSound("SolidMetal.BulletImpact", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
CDynamicProp? cloneProp = null;
if (playerSkill.CloneProp != null)
@ -468,8 +468,9 @@ namespace src.player.skills
public List<ulong> Weapons { get; set; } = [];
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#d0d930", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float cooldown = 30, float duration = 10) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#d0d930", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float cooldown = 30, float duration = 10, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
public float Duration { get; set; } = duration;
}

View file

@ -50,11 +50,19 @@ namespace src.player.skills
private static void SendAlertToAll()
{
holdersTick = int.MinValue;
foreach (var player in PlayerManager.GetTickPlayers())
{
if (!CheckIlliterateSkill(player)) continue;
player.PrintToCenterAlert(player.GetTranslationWithoutIlliterate("illiterate_alert"));
}
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) continue;
string alert = playerEvent.GetTranslationWithoutIlliterate("illiterate_alert");
playerEvent.PrintToCenterAlert(alert);
SkillUtils.PrintToChat(playerEvent, $"{ChatColors.Red}{alert}", ignoreIlliterate: true);
}
}
public static bool CheckIlliterateSkill(CCSPlayerController? player)

View file

@ -199,7 +199,7 @@ namespace src.player.skills
if (!consumedReplicas.TryAdd(replica.Index, 0)) return;
replica.EmitSound("GlassBottle.BulletImpact", volume: 1f);
replica.EmitSound("GlassBottle.BulletImpact", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
if (ActiveTimers.TryRemove((int)replica.Index, out var timer)) timer?.Kill();
var owner = GetReplicaOwner(replica.Index);
@ -230,8 +230,9 @@ namespace src.player.skills
public DateTime Cooldown { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#42f5ef", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Common, float cooldown = 30f, float durationRun = 5, float durationCrouch = 12, int yourTeamDamage = 10, int enemyTeamDamage = 20) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#42f5ef", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Common, float cooldown = 30f, float durationRun = 5, float durationCrouch = 12, int yourTeamDamage = 10, int enemyTeamDamage = 20, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
public float DurationRun { get; set; } = durationRun;
public float DurationCrouch { get; set; } = durationCrouch;

View file

@ -9,6 +9,7 @@ namespace src.player.skills
public class Jammer : ISkill
{
private const Skills skillName = Skills.Jammer;
private const string ownerTag = "Jammer";
private static readonly ConcurrentDictionary<uint, byte> jammedPlayers = [];
private static readonly ConcurrentDictionary<uint, uint> jammerToTarget = [];
private static readonly object setLock = new();
@ -118,14 +119,7 @@ namespace src.player.skills
private static void SetCrosshair(CCSPlayerController player, bool enabled)
{
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid) return;
pawn.HideHUD = (uint)(enabled
? (pawn.HideHUD & ~(1 << 8))
: (pawn.HideHUD | (1 << 8)));
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_iHideHUD");
SkillUtils.SetCrosshairHidden(player, ownerTag, !enabled);
}
public static void BotTakeover(EventBotTakeover @event)

View file

@ -12,6 +12,7 @@ namespace src.player.skills
public class Jester : ISkill
{
private const Skills skillName = Skills.Jester;
private const string toggleSound = "Weapon_Taser.ChargeReady";
private static readonly ConcurrentDictionary<uint, JesterInfo> jesters = [];
public static void LoadSkill()
@ -196,7 +197,7 @@ namespace src.player.skills
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent != null && playerEvent.IsValid)
playerEvent.ExecuteClientCommand("play sounds/weapons/taser/taser_charge_ready");
SkillUtils.EmitSoundToPlayer(playerEvent, toggleSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
var minTime = SkillsInfo.GetValue<float>(skillName, "minTime");
@ -275,8 +276,9 @@ namespace src.player.skills
public Timer? Timer { get; set; } = null;
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#8f108f", 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, float minTime = 10f, float maxTime = 25f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#8f108f", 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, float minTime = 10f, float maxTime = 25f, float soundVolume = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float MinTime { get; set; } = minTime;
public float MaxTime { get; set; } = maxTime;
}

View file

@ -166,6 +166,8 @@ namespace src.player.skills
private static void PushPlayer(CCSPlayerController player)
{
if (Heavyweight.Resists(player)) return;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid || playerPawn.Health <= 0) return;

View file

@ -9,6 +9,7 @@ namespace src.player.skills
public class LastGasp : ISkill
{
private const Skills skillName = Skills.LastGasp;
private const string hurtSound = "Player.DamageBody.Victim";
public static void LoadSkill()
{
@ -45,11 +46,12 @@ namespace src.player.skills
}
SkillUtils.TakeHealth(attackerPawn, damageAfterDeath, victim, KillfeedIcons.Fist);
PlayerManager.GetPlayerFromEvent(attacker)?.ExecuteClientCommand($"play player/player_damagebody_0{jRandomSkills.Instance.Random.Next(4, 8)}");
SkillUtils.EmitSoundToPlayer(attacker, hurtSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#88bdba", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, int damageAfterDeath = 30, bool canKill = true) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#88bdba", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, int damageAfterDeath = 30, bool canKill = true, float soundVolume = .35f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int DamageAfterDeath { get; set; } = damageAfterDeath;
public bool CanKill { get; set; } = canKill;

View file

@ -12,6 +12,9 @@ namespace src.player.skills
public class LongKnife : ISkill
{
private const Skills skillName = Skills.LongKnife;
private const string victimSound = "Player.DamageBody.Victim";
private const string heavyHitSound = "Weapon_Knife.Hit.Heavy.Flesh";
private const string lightHitSound = "Weapon_Knife.Hit.Light.Flesh";
private static bool hooked = false;
private const int actionCode = 503;
@ -121,12 +124,15 @@ namespace src.player.skills
if (!SkillsInfo.GetValue<bool>(skillName, "friendlyFire") && player.Team == target.Team) return;
target.PlayerPawn.Value.EmitSound("Player.DamageBody.Onlooker");
SkillUtils.EmitSoundToPlayer(target, victimSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
SkillUtils.EmitSoundToPlayer(player, heavyHit ? heavyHitSound : lightHitSound, SkillsInfo.GetValue<float>(skillName, "hitSoundVolume"));
SkillUtils.TakeHealth(target.PlayerPawn.Value, heavyHit ? Instance.Random.Next(45, 55) : Instance.Random.Next(21, 34), player, killfeedIcon ?? KillfeedIcons.Knife);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#c9f8ff", 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, float maxDistance = 4096f, bool friendlyFire = true) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#c9f8ff", 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, float maxDistance = 4096f, bool friendlyFire = true, float soundVolume = .35f, float hitSoundVolume = .35f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float HitSoundVolume { get; set; } = hitSoundVolume;
public float MaxDistance { get; set; } = maxDistance;
public bool FriendlyFire { get; set; } = friendlyFire;
}

View file

@ -16,11 +16,19 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
DecoyRing.PreloadAssets();
}
public static void NewRound()
{
decoys.Clear();
DecoyRing.ClearAll(skillName);
}
public static void RoundEnd()
{
decoys.Clear();
DecoyRing.ClearAll(skillName);
}
public static void DecoyStarted(EventDecoyStarted @event)
@ -31,7 +39,9 @@ namespace src.player.skills
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
decoys.TryAdd(new Vector(@event.X, @event.Y, @event.Z), 0);
Vector pos = new(@event.X, @event.Y, @event.Z);
decoys.TryAdd(pos, 0);
DecoyRing.Show(skillName, (uint)@event.Entityid, pos, SkillsInfo.GetValue<float>(skillName, "triggerRadius"));
}
public static void DecoyDetonate(EventDecoyDetonate @event)
@ -44,6 +54,8 @@ namespace src.player.skills
foreach (var decoy in decoys.Keys.Where(v => v.X == @event.X && v.Y == @event.Y && v.Z == @event.Z))
decoys.TryRemove(decoy, out _);
DecoyRing.Hide(skillName, (uint)@event.Entityid);
}
public static void OnTick()

View file

@ -93,7 +93,7 @@ namespace src.player.skills
skillInfo.Cooldown = DateTime.Now;
skillInfo.Count -= 1;
SkillUtils.AddHealth(playerPawn, SkillsInfo.GetValue<int>(skillName, "healthToAdd"));
player.EmitSound("Healthshot.Success");
SkillUtils.EmitSoundToPlayer(player, "Healthshot.Success", SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
}
@ -106,8 +106,9 @@ namespace src.player.skills
public DateTime Cooldown { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#10c212", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, int healthToAdd = 50, int healthShotLimit = 3, float cooldown = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#10c212", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, int healthToAdd = 50, int healthShotLimit = 3, float cooldown = 1f, float soundVolume = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int HealthToAdd { get; set; } = healthToAdd;
public int HealthShotLimit { get; set; } = healthShotLimit;
public float Cooldown { get; set; } = cooldown;

View file

@ -68,7 +68,7 @@ namespace src.player.skills
position.Z += 60;
grenade.Teleport(position);
grenade.EmitSound("IncGrenade.Bounce_M");
grenade.EmitSound("IncGrenade.Bounce_M", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
grenade.DetonateTime = Server.CurrentTime + .5f;
Utilities.SetStateChanged(grenade, "CBaseGrenade", "m_flDetonateTime");
@ -161,8 +161,9 @@ namespace src.player.skills
SkillUtils.UpdateGrenadeCount(player, CsItem.HEGrenade, 1);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#adf542", 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, float detonationRange = 130, int grenadeLimit = 3) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#adf542", 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, float detonationRange = 130, int grenadeLimit = 3, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float DetonationRange { get; set; } = detonationRange;
public int GrenadeLimit { get; set; } = grenadeLimit;
}

View file

@ -0,0 +1,156 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
using static src.jRandomSkills;
namespace src.player.skills
{
public class Nemesis : ISkill
{
private const Skills skillName = Skills.Nemesis;
private static readonly ConcurrentDictionary<uint, byte> markedPlayers = [];
private static readonly ConcurrentDictionary<uint, uint> ownerToTarget = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
markedPlayers.Clear();
ownerToTarget.Clear();
foreach (var player in PlayerManager.GetTickPlayers())
SkillUtils.CloseMenu(player);
}
public static void PlayerDisconnect(uint playerIndex)
{
markedPlayers.TryRemove(playerIndex, out _);
ownerToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in ownerToTarget)
if (kvp.Value == playerIndex)
ownerToTarget.TryRemove(kvp.Key, out _);
}
public static void EnableSkill(CCSPlayerController player)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
playerInfo.SkillUsed = false;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
var enemies = SkillUtils.GetSelectableEnemies(player, true);
if (enemies.Length > 0)
{
ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString())));
SkillUtils.CreateMenu(player, menuItems);
}
else
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
if (ownerToTarget.TryRemove(player.Index, out uint targetIndex))
markedPlayers.TryRemove(targetIndex, out _);
SkillUtils.CloseMenu(player);
}
public static void OnTick()
{
if (Server.TickCount % 32 != 0) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null || playerInfo.Skill != skillName) continue;
if (!SkillUtils.HasMenu(player)) continue;
var enemies = SkillUtils.GetSelectableEnemies(player, true);
ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString())));
SkillUtils.UpdateMenu(player, menuItems);
}
}
public static void TypeSkill(CCSPlayerController player, string[] commands)
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
if (playerInfo.SkillUsed)
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("areareaper_used_info")}");
return;
}
if (commands.Length == 0 || !uint.TryParse(commands[0], out uint enemyIndex))
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
return;
}
var enemy = Utilities.GetPlayerFromIndex((int)enemyIndex);
if (enemy == null || !enemy.IsValid)
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
return;
}
markedPlayers[enemy.Index] = 0;
ownerToTarget[player.Index] = enemy.Index;
playerInfo.SkillUsed = true;
playerEvent.PrintToChat($" {ChatColors.Green}{playerEvent.GetTranslation("nemesis_player_info", enemy.PlayerName)}");
var enemyEvent = PlayerManager.GetPlayerFromEvent(enemy);
if (enemyEvent != null && enemyEvent.IsValid)
enemyEvent.PrintToChat($" {ChatColors.Red}{enemyEvent.GetTranslation("nemesis_enemy_info")}");
}
public static void OnTakeDamage(DynamicHook h)
{
if (markedPlayers.IsEmpty) return;
CEntityInstance param = h.GetParam<CEntityInstance>(0);
CTakeDamageInfo param2 = h.GetParam<CTakeDamageInfo>(1);
if (param == null || !param.IsValid || param2 == null) return;
var victimPawn = param.As<CCSPlayerPawn>();
if (victimPawn == null || !victimPawn.IsValid || victimPawn.DesignerName != "player") return;
var victimController = victimPawn.Controller.Value;
if (victimController == null || !victimController.IsValid) return;
var victim = victimController.As<CCSPlayerController>();
if (victim == null || !victim.IsValid) return;
uint victimIndex = PlayerManager.GetPlayerEvent(victim)?.Index ?? victim.Index;
if (!markedPlayers.ContainsKey(victimIndex)) return;
param2.Damage *= 1f + SkillsInfo.GetValue<float>(skillName, "extraDamagePercent");
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#b8003a", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Rare, float extraDamagePercent = .25f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float ExtraDamagePercent { get; set; } = extraDamagePercent;
}
}
}

View file

@ -10,6 +10,7 @@ namespace src.player.skills
public class Nightmare : ISkill
{
private const Skills skillName = Skills.Nightmare;
private const string cursedSound = "UI.ArmsRace.FinalKill_Tone";
private static readonly ConcurrentDictionary<uint, uint> playersToTarget = [];
private static readonly ConcurrentDictionary<uint, uint> targetVolumes = [];
private static readonly object setLock = new();
@ -129,7 +130,10 @@ namespace src.player.skills
playerEvent.PrintToChat($" {ChatColors.Green}" + playerEvent.GetTranslation("nightmare_player_info", enemy.PlayerName));
if (enemyEvent != null && enemyEvent.IsValid)
{
enemyEvent.PrintToChat($" {ChatColors.Red}" + enemyEvent.GetTranslation("nightmare_enemy_info"));
SkillUtils.EmitSoundToPlayer(enemyEvent, cursedSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
public static void DisableSkill(CCSPlayerController player)
@ -227,8 +231,9 @@ namespace src.player.skills
EntityManager.DestroyEntity(volumeIndex);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5b2c6f", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, string postProcessing = "lighting/postprocessing/effects/death_cam_phase1_low_violence.vpost", float fadeTime = .25f, float minExposure = .5f, float maxExposure = 2f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5b2c6f", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, string postProcessing = "lighting/postprocessing/effects/death_cam_phase1_low_violence.vpost", float fadeTime = .25f, float minExposure = .5f, float maxExposure = 2f, float soundVolume = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public string PostProcessing { get; set; } = postProcessing;
public float FadeTime { get; set; } = fadeTime;
public float MinExposure { get; set; } = minExposure;

View file

@ -63,37 +63,29 @@ namespace src.player.skills
var bomb = PlayerManager.GetTickBomb();
uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null;
var hidden = SkillUtils.ResolveHiddenPawns(invisiblePlayers.Keys, bombOwnerIndex);
if (hidden.Count == 0) return;
foreach (var (info, player) in infoList)
{
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero;
foreach (var playerIndex in invisiblePlayers.Keys)
foreach (var target in hidden)
{
var playerController = PlayerManager.GetPlayerEvent(Utilities.GetPlayerFromIndex((int)playerIndex));
if (playerController == null || !playerController.IsValid || playerController.Index == player.Index)
continue;
if (player.Team == playerController.Team)
continue;
var playerPawn = playerController.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid) continue;
if (target.Index == player.Index) continue;
if (player.Team == target.Team) continue;
// Only the actively spectated pawn stays transmitted; hiding it breaks the camera.
if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle)
continue;
if (targetHandle != nint.Zero && target.Pawn.Handle == targetHandle) continue;
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
if (entity == null || !entity.IsValid) continue;
if (info.TransmitEntities.Contains(target.Pawn.Index))
info.TransmitEntities.Remove(target.Pawn.Index);
if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index);
SkillUtils.HideCarriedEntities(info, target.Pawn);
SkillUtils.HideCarriedEntities(info, playerPawn);
if (bomb == null || bombOwnerIndex != playerController.Index) continue;
if (bomb == null || !target.HoldsBomb) continue;
if (info.TransmitEntities.Contains(bomb.Index))
info.TransmitEntities.Remove(bomb.Index);

View file

@ -15,15 +15,14 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
private static readonly HashSet<string> blockedWeapons =
["hegrenade", "inferno", "decoy", "flashbang", "smokegrenade", "molotov"];
private const string infernoDamage = "inferno";
public static bool PlayerHurtPre(EventPlayerHurt @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (!Instance.IsPlayerValid(player)) return false;
if (!blockedWeapons.Contains(@event.Weapon)) return false;
if (@event.Weapon != infernoDamage && !WeaponPool.IsGrenade(@event.Weapon)) return false;
if (PlayerManager.GetPlayerByIndex(player!.Index)?.Skill != skillName) return false;
SkillUtils.RestoreHealth(player);

View file

@ -9,6 +9,7 @@ namespace src.player.skills
public class PawelJumper : ISkill
{
private const Skills skillName = Skills.PawelJumper;
private const string jumpSound = "Default.WalkJump";
private static readonly int?[] J = new int?[64];
private static readonly PlayerButtons[] LB = new PlayerButtons[64];
@ -73,13 +74,17 @@ namespace src.player.skills
{
J[player.Slot]++;
playerEventPawn.AbsVelocity.Z = 300;
if (J[player.Slot] > 1)
playerEventPawn.EmitSound(jumpSound, volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
LB[player.Slot] = buttons;
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#FFA500", 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 extraJumpsMin = 1, int extraJumpsMax = 4) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#FFA500", 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 extraJumpsMin = 1, int extraJumpsMax = 4, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int ExtraJumpsMin { get; set; } = extraJumpsMin;
public int ExtraJumpsMax { get; set; } = extraJumpsMax;
}

View file

@ -15,13 +15,29 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
Instance.AddToManifest(SkillsInfo.GetValue<string>(skillName, "particleName"));
}
public static void NewRound()
{
foreach (var pilotInfo in PlayerPilotInfo.Values)
ClearTrail(pilotInfo);
PlayerPilotInfo.Clear();
}
public static void PlayerDeath(EventPlayerDeath @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
if (PlayerPilotInfo.TryGetValue(player.Index, out var pilotInfo))
{
ClearTrail(pilotInfo);
pilotInfo.IsFlying = false;
}
}
public static void OnTick()
{
foreach (var player in PlayerManager.GetTickPlayers())
@ -39,7 +55,9 @@ namespace src.player.skills
public static void DisableSkill(CCSPlayerController player)
{
PlayerPilotInfo.TryRemove(player.Index, out _);
if (PlayerPilotInfo.TryRemove(player.Index, out var pilotInfo))
ClearTrail(pilotInfo);
SkillUtils.ResetPrintHTML(player);
}
@ -96,9 +114,18 @@ namespace src.player.skills
);
if (inUse)
{
ApplyPilotEffect(player);
else if (pilotInfo.Fuel <= 0)
pilotInfo.IsFlying = false;
StartTrail(player, playerPawn, pilotInfo);
}
else
{
if (pilotInfo.TrailIndex != null)
ClearTrail(pilotInfo);
if (pilotInfo.Fuel <= 0)
pilotInfo.IsFlying = false;
}
pilotInfo.LastButtons = buttons;
UpdateHUD(player, pilotInfo);
@ -132,6 +159,62 @@ namespace src.player.skills
return "#FF0000";
}
private static void StartTrail(CCSPlayerController player, CCSPlayerPawn pawn, Pilot_PlayerInfo pilotInfo)
{
if (pilotInfo.TrailIndex != null)
{
var existing = Utilities.GetEntityFromIndex<CParticleSystem>((int)pilotInfo.TrailIndex.Value);
if (existing != null && existing.IsValid) return;
pilotInfo.TrailIndex = null;
}
if (pawn.AbsOrigin == null) return;
var particle = EntityManager.CreateTrackedParticleSystem(player.Index, SkillsInfo.GetValue<string>(skillName, "particleName"));
if (particle == null || !particle.IsValid) return;
float offset = SkillsInfo.GetValue<float>(skillName, "particleOffset");
particle.Teleport(new Vector(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + offset));
particle.AcceptInput("SetParent", pawn, particle, "!activator");
particle.AcceptInput("Start");
pilotInfo.TrailIndex = particle.Index;
}
private static void StopTrail(Pilot_PlayerInfo pilotInfo)
{
if (pilotInfo.TrailIndex == null) return;
uint index = pilotInfo.TrailIndex.Value;
pilotInfo.TrailIndex = null;
var particle = Utilities.GetEntityFromIndex<CParticleSystem>((int)index);
if (particle != null && particle.IsValid)
{
particle.AcceptInput("Stop");
particle.AcceptInput("DestroyImmediately");
}
EntityManager.DestroyEntity(index);
if (EntityManager.SuppressKills) return;
Server.NextFrame(() =>
{
var leftover = Utilities.GetEntityFromIndex<CParticleSystem>((int)index);
if (leftover != null && leftover.IsValid)
leftover.Remove();
});
}
private static void ClearTrail(Pilot_PlayerInfo pilotInfo)
{
StopTrail(pilotInfo);
foreach (var leftover in EntityManager.GetPlayerEntities((uint)pilotInfo.SteamID, "particle_system"))
EntityManager.DestroyEntity(leftover);
}
private static void ApplyPilotEffect(CCSPlayerController player)
{
var playerPawn = player.PlayerPawn.Value;
@ -168,10 +251,13 @@ namespace src.player.skills
public int JumpCount { get; set; } = 0;
public float LastJumpTime { get; set; } = 0;
public bool IsFlying { get; set; } = false;
public uint? TrailIndex { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#1466F5", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float maximumFuel = 150f, float fuelConsumption = .64f, float refuelling = .1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#1466F5", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float maximumFuel = 150f, float fuelConsumption = .64f, float refuelling = .1f, string particleName = "particles/inferno_fx/incgrenade_thrown_trail.vpcf", float particleOffset = 0f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public string ParticleName { get; set; } = particleName;
public float ParticleOffset { get; set; } = particleOffset;
public float MaximumFuel { get; set; } = maximumFuel;
public float FuelConsumption { get; set; } = fuelConsumption;
public float Refuelling { get; set; } = refuelling;

View file

@ -94,6 +94,8 @@ namespace src.player.skills
public static void OnTick()
{
float currentTime = Server.CurrentTime;
bool hudFrame = SkillUtils.IsHudFrame();
foreach (var player in PlayerManager.GetTickPlayers().Where(p => p.Team == CsTeam.Terrorist))
{
if (!Instance.IsPlayerValid(player)) continue;
@ -111,7 +113,7 @@ namespace src.player.skills
pawn.InBombZone = true;
Schema.SetSchemaValue<bool>(pawn.Handle, "CCSPlayerPawn", "m_bInBombZone", true);
if (plantingPlayers.TryGetValue(player.Index, out float plantTime))
if (hudFrame && plantingPlayers.TryGetValue(player.Index, out float plantTime))
{
float remaining = plantTime + 3f - currentTime;
playerInfo.PrintHTML = $"{player.GetTranslation("planter_planting", $"<font color='#00FF00'>{Math.Max(0, remaining):0.0}s</font>")}";

View file

@ -10,6 +10,7 @@ namespace src.player.skills
public class Poison : ISkill
{
private const Skills skillName = Skills.Poison;
private const string poisonSound = "Player.DamageBody.Victim";
private static readonly ConcurrentDictionary<uint, byte> poisonedPlayers = [];
private static readonly ConcurrentDictionary<uint, uint> playersToTarget = [];
private static readonly ConcurrentDictionary<uint, uint> targetToPlayer = [];
@ -69,7 +70,7 @@ namespace src.player.skills
SkillUtils.TakeHealth(pawn, SkillsInfo.GetValue<int>(skillName, "Damage"), GetSkillOwner(playerIndex), KillfeedIcons.Spray);
if (Server.TickCount % cooldown2 == 0)
PlayerManager.GetPlayerFromEvent(player)?.ExecuteClientCommand($"play player/player_damagebody_0{jRandomSkills.Instance.Random.Next(4, 8)}");
SkillUtils.EmitSoundToPlayer(player, poisonSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
@ -186,8 +187,9 @@ namespace src.player.skills
SkillUtils.CloseMenu(player);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#902eff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Common, float cooldown = .85f, int damage = 1, int minHealth = 30) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#902eff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Common, float cooldown = .85f, int damage = 1, int minHealth = 30, float soundVolume = .3f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int Damage { get; set; } = damage;
public float Cooldown { get; set; } = cooldown;
public int MinHealth { get; set; } = minHealth;

View file

@ -12,16 +12,7 @@ namespace src.player.skills
private static readonly ConcurrentDictionary<uint, byte> bannedPlayers = [];
private static readonly ConcurrentDictionary<uint, uint> playersToTarget = [];
private static readonly object setLock = new();
private static readonly string[] disabledWeapons =
[
"weapon_ak47", "weapon_m4a4", "weapon_m4a1", "weapon_m4a1_silencer",
"weapon_famas", "weapon_galilar", "weapon_aug", "weapon_sg556",
"weapon_mp9", "weapon_mac10", "weapon_bizon", "weapon_mp7",
"weapon_ump45", "weapon_p90", "weapon_mp5sd", "weapon_ssg08",
"weapon_awp", "weapon_scar20", "weapon_g3sg1", "weapon_nova",
"weapon_xm1014", "weapon_mag7", "weapon_sawedoff", "weapon_m249",
"weapon_negev"
];
private static HashSet<string> disabledWeapons => WeaponPool.Rifles;
public static void LoadSkill()
{

View file

@ -69,7 +69,7 @@ namespace src.player.skills
}
if (!info.Defusing)
pawn.EmitSound("c4.disarmstart");
pawn.EmitSound("c4.disarmstart", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
info.Defusing = true;
info.DefusingTime -= (1f / tickRate);
@ -128,8 +128,9 @@ namespace src.player.skills
public float DefusingTime { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#507529", CsTeam onlyTeam = CsTeam.CounterTerrorist, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float maxDefusingRange = 80f, float defusingTime = 10f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#507529", CsTeam onlyTeam = CsTeam.CounterTerrorist, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float maxDefusingRange = 80f, float defusingTime = 10f, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float MaxDefusingRange { get; set; } = maxDefusingRange;
public float DefusingTime { get; set; } = defusingTime;
}

View file

@ -0,0 +1,110 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
namespace src.player.skills
{
public class Punisher : ISkill
{
private const Skills skillName = Skills.Punisher;
private static readonly ConcurrentDictionary<uint, float> storedDamage = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
storedDamage.Clear();
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
storedDamage[player.Index] = 0f;
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
storedDamage.TryRemove(player.Index, out _);
SkillUtils.ResetPrintHTML(player);
}
public static void PlayerDisconnect(uint playerIndex)
{
storedDamage.TryRemove(playerIndex, out _);
}
public static void PlayerDeath(EventPlayerDeath @event)
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return;
if (storedDamage.ContainsKey(victim.Index))
storedDamage[victim.Index] = 0f;
}
public static void OnTakeDamage(DynamicHook h)
{
if (storedDamage.IsEmpty) return;
CEntityInstance param = h.GetParam<CEntityInstance>(0);
CTakeDamageInfo param2 = h.GetParam<CTakeDamageInfo>(1);
if (param == null || !param.IsValid || param2 == null) return;
var victimPawn = param.As<CCSPlayerPawn>();
if (victimPawn == null || !victimPawn.IsValid || victimPawn.DesignerName != "player") return;
var victimController = victimPawn.Controller.Value;
if (victimController == null || !victimController.IsValid) return;
var victim = victimController.As<CCSPlayerController>();
if (victim == null || !victim.IsValid) return;
uint victimIndex = PlayerManager.GetPlayerEvent(victim)?.Index ?? victim.Index;
var attacker = param2.Attacker?.Value?.As<CCSPlayerPawn>();
var attackerController = attacker?.Controller?.Value?.As<CCSPlayerController>();
uint? attackerIndex = attackerController != null && attackerController.IsValid
? PlayerManager.GetPlayerEvent(attackerController)?.Index ?? attackerController.Index
: null;
if (attackerIndex.HasValue && attackerIndex.Value != victimIndex && storedDamage.TryGetValue(attackerIndex.Value, out float banked) && banked > 0f)
{
storedDamage[attackerIndex.Value] = 0f;
param2.Damage += banked;
var puncher = CounterStrikeSharp.API.Utilities.GetPlayerFromIndex((int)attackerIndex.Value);
var puncherPawn = puncher?.PlayerPawn?.Value;
if (puncherPawn != null && puncherPawn.IsValid && puncherPawn.Health > 0)
SkillUtils.SetHealth(puncherPawn, puncherPawn.Health + (int)MathF.Round(banked));
var puncherEvent = PlayerManager.GetPlayerFromEvent(puncher);
if (puncherEvent != null && puncherEvent.IsValid)
puncherEvent.PrintToChat($" {ChatColors.Green}{puncherEvent.GetTranslation("punisher_released_info", (int)MathF.Round(banked))}");
}
if (!storedDamage.ContainsKey(victimIndex)) return;
if (attackerIndex.HasValue && attackerIndex.Value == victimIndex) return;
float ratio = SkillsInfo.GetValue<float>(skillName, "storePercent");
float cap = SkillsInfo.GetValue<float>(skillName, "maxStored");
float stored = storedDamage[victimIndex] + (param2.Damage * ratio);
storedDamage[victimIndex] = MathF.Min(stored, cap);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ffb347", 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, float storePercent = .5f, float maxStored = 100f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float StorePercent { get; set; } = storePercent;
public float MaxStored { get; set; } = maxStored;
}
}
}

View file

@ -23,6 +23,8 @@ namespace src.player.skills
if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim)
return;
if (Heavyweight.Resists(victim)) return;
var playerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index);
if (playerInfo?.Skill == skillName && victim!.PawnIsAlive)

View file

@ -13,19 +13,8 @@ namespace src.player.skills
private static readonly ConcurrentDictionary<uint, PlayerSkillInfo> SkillPlayerInfo = new();
private static readonly object setLock = new();
private static readonly HashSet<string> pistols = new(StringComparer.Ordinal)
{
"weapon_deagle", "weapon_revolver", "weapon_glock", "weapon_usp_silencer",
"weapon_cz75a", "weapon_fiveseven", "weapon_p250", "weapon_tec9", "weapon_elite", "weapon_hkp2000"
};
private static readonly HashSet<string> rifles = new(StringComparer.Ordinal)
{
"weapon_mp9", "weapon_mac10", "weapon_bizon", "weapon_mp7", "weapon_ump45", "weapon_p90",
"weapon_mp5sd", "weapon_famas", "weapon_galilar", "weapon_m4a1", "weapon_m4a1_silencer", "weapon_ak47",
"weapon_aug", "weapon_sg556", "weapon_ssg08", "weapon_awp", "weapon_scar20", "weapon_g3sg1",
"weapon_nova", "weapon_xm1014", "weapon_mag7", "weapon_sawedoff", "weapon_m249", "weapon_negev"
};
private static HashSet<string> pistols => WeaponPool.Pistols;
private static HashSet<string> rifles => WeaponPool.Rifles;
public static void LoadSkill()
{
@ -104,6 +93,19 @@ namespace src.player.skills
RemoveAndGiveWeapon(player);
}
private static bool RollPistol(bool pistolAvailable, bool rifleAvailable)
{
if (!rifleAvailable) return true;
if (!pistolAvailable) return false;
float pistolChance = Math.Max(SkillsInfo.GetValue<float>(skillName, "pistolChance"), 0);
float rifleChance = Math.Max(SkillsInfo.GetValue<float>(skillName, "rifleChance"), 0);
float total = pistolChance + rifleChance;
if (total <= 0) return Instance.Random.Next(2) == 0;
return Instance.Random.NextDouble() * total < pistolChance;
}
private static void RemoveAndGiveWeapon(CCSPlayerController player)
{
var pawn = player.PlayerPawn.Value;
@ -117,12 +119,15 @@ namespace src.player.skills
if (playerWeapons.Count == 0)
return;
var available = pistols.Concat(rifles).Where(w => !playerWeapons.Contains(w)).ToArray();
if (available.Length == 0)
var availablePistols = pistols.Where(w => !playerWeapons.Contains(w)).ToArray();
var availableRifles = rifles.Where(w => !playerWeapons.Contains(w)).ToArray();
if (availablePistols.Length == 0 && availableRifles.Length == 0)
return;
bool isPistol = RollPistol(availablePistols.Length > 0, availableRifles.Length > 0);
var available = isPistol ? availablePistols : availableRifles;
string newWeapon = available[Instance.Random.Next(available.Length)];
bool isPistol = pistols.Contains(newWeapon);
string? weaponToRemove = null;
foreach (var item in pawn.WeaponServices.MyWeapons)
@ -163,9 +168,11 @@ namespace src.player.skills
public DateTime Cooldown { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#e0873a", 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, float cooldown = 15f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#e0873a", 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, float cooldown = 15f, float pistolChance = 60f, float rifleChance = 40f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float Cooldown { get; set; } = cooldown;
public float PistolChance { get; set; } = pistolChance;
public float RifleChance { get; set; } = rifleChance;
}
}
}

View file

@ -152,7 +152,7 @@ namespace src.player.skills
if (!consumedReplicas.TryAdd(replica.Index, 0)) return;
replica.EmitSound("GlassBottle.BulletImpact", volume: 1f);
replica.EmitSound("GlassBottle.BulletImpact", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
var owner = GetReplicaOwner(replica.Index);
EntityManager.DestroyEntity(replica.Index);
@ -183,8 +183,9 @@ namespace src.player.skills
public DateTime Cooldown { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#a3000b", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Common, float cooldown = 15f, int yourTeamDamage = 10, int enemyTeamDamage = 20) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#a3000b", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Common, float cooldown = 15f, int yourTeamDamage = 10, int enemyTeamDamage = 20, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
public int YourTeamDamage { get; set; } = yourTeamDamage;
public int EnemyTeamDamage { get; set; } = enemyTeamDamage;

View file

@ -0,0 +1,205 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
using System.Drawing;
namespace src.player.skills
{
public class Rewind : ISkill
{
private const Skills skillName = Skills.Rewind;
private static readonly ConcurrentDictionary<uint, PlayerSkillInfo> SkillPlayerInfo = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
foreach (var skillInfo in SkillPlayerInfo.Values)
DestroyMarker(skillInfo);
SkillPlayerInfo.Clear();
}
public static void RoundEnd()
{
foreach (var skillInfo in SkillPlayerInfo.Values)
DestroyMarker(skillInfo);
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
SkillPlayerInfo[player.Index] = new PlayerSkillInfo
{
CanUse = true,
Cooldown = DateTime.MinValue,
};
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
if (SkillPlayerInfo.TryRemove(player.Index, out var skillInfo))
DestroyMarker(skillInfo);
SkillUtils.ResetPrintHTML(player);
}
public static void PlayerDisconnect(uint playerIndex)
{
if (SkillPlayerInfo.TryRemove(playerIndex, out var skillInfo))
DestroyMarker(skillInfo);
}
public static void PlayerDeath(EventPlayerDeath @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return;
if (SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo))
{
skillInfo.ReturnTick = 0;
DestroyMarker(skillInfo);
}
}
public static void UseSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
if (!SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo)) return;
if (!skillInfo.CanUse || skillInfo.ReturnTick != 0) return;
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null) return;
skillInfo.Mark = new Vector(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z);
skillInfo.MarkAngle = new QAngle(0, pawn.V_angle.Y, 0);
skillInfo.ReturnTick = Server.TickCount + (int)(SkillsInfo.GetValue<float>(skillName, "returnDelay") * 64);
skillInfo.CanUse = false;
skillInfo.Cooldown = DateTime.Now;
SpawnMarker(player, skillInfo);
SkillUtils.EmitSoundToPlayer(player, "UIPanorama.buttonclick", SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
public static void OnTick()
{
if (SkillPlayerInfo.IsEmpty) return;
bool hudFrame = SkillUtils.IsHudFrame();
foreach (var player in PlayerManager.GetTickPlayers())
{
if (player == null || !player.IsValid) continue;
if (!SkillPlayerInfo.TryGetValue(player.Index, out var skillInfo)) continue;
if (PlayerManager.GetPlayerByIndex(player.Index)?.Skill != skillName) continue;
if (skillInfo.ReturnTick != 0 && Server.TickCount >= skillInfo.ReturnTick)
{
skillInfo.ReturnTick = 0;
Teleport(player, skillInfo);
DestroyMarker(skillInfo);
}
if (hudFrame)
UpdateHUD(player, skillInfo);
}
}
private static void Teleport(CCSPlayerController player, PlayerSkillInfo skillInfo)
{
if (skillInfo.Mark == null) return;
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE)
{
skillInfo.Mark = null;
return;
}
pawn.Teleport(skillInfo.Mark, skillInfo.MarkAngle, new Vector(0, 0, 0));
skillInfo.Mark = null;
}
private static void SpawnMarker(CCSPlayerController player, PlayerSkillInfo skillInfo)
{
if (skillInfo.Mark == null) return;
if (EntityManager.OverBudget()) return;
Color color = Color.White;
try { color = ColorTranslator.FromHtml(SkillsInfo.GetValue<string>(skillName, "color")); }
catch { }
Vector bottom = new(skillInfo.Mark.X, skillInfo.Mark.Y, skillInfo.Mark.Z + 2);
Vector top = new(skillInfo.Mark.X, skillInfo.Mark.Y, skillInfo.Mark.Z + 68);
var beam = EntityManager.CreateTrackedBeam(player.Index, bottom, top, color);
if (beam == null || !beam.IsValid) return;
beam.Width = .6f;
beam.EndWidth = .6f;
Utilities.SetStateChanged(beam, "CBeam", "m_fWidth");
Utilities.SetStateChanged(beam, "CBeam", "m_fEndWidth");
skillInfo.MarkerIndex = beam.Index;
}
private static void DestroyMarker(PlayerSkillInfo skillInfo)
{
if (skillInfo.MarkerIndex == null) return;
uint markerIndex = skillInfo.MarkerIndex.Value;
skillInfo.MarkerIndex = null;
EntityManager.DestroyBeam(markerIndex);
}
private static void UpdateHUD(CCSPlayerController player, PlayerSkillInfo skillInfo)
{
float time = (int)Math.Ceiling((skillInfo.Cooldown.AddSeconds(SkillsInfo.GetValue<float>(skillName, "cooldown")) - DateTime.Now).TotalSeconds);
float cooldown = Math.Max(time, 0);
if (cooldown == 0 && !skillInfo.CanUse)
skillInfo.CanUse = true;
var playerInfo = PlayerManager.GetPlayerByIndex(player.Index);
if (playerInfo == null) return;
if (skillInfo.ReturnTick != 0)
{
int remaining = (int)Math.Ceiling((skillInfo.ReturnTick - Server.TickCount) / 64f);
playerInfo.PrintHTML = $"<font color='#00FF00'>{player.GetTranslation("rewind_pending_info", Math.Max(remaining, 0))}</font>";
}
else if (cooldown == 0)
playerInfo.PrintHTML = null;
else
playerInfo.PrintHTML = $"{player.GetTranslation("hud_info", $"<font color='#FF0000'>{cooldown}</font>")}";
}
public class PlayerSkillInfo
{
public bool CanUse { get; set; }
public DateTime Cooldown { get; set; }
public Vector? Mark { get; set; }
public QAngle? MarkAngle { get; set; }
public uint? MarkerIndex { get; set; }
public int ReturnTick { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#4fc3f7", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Uncommon, float returnDelay = 4f, float cooldown = 30f, float soundVolume = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float ReturnDelay { get; set; } = returnDelay;
public float Cooldown { get; set; } = cooldown;
}
}
}

View file

@ -14,6 +14,7 @@ namespace src.player.skills
public class Ricochet : ISkill
{
private const Skills skillName = Skills.Ricochet;
private const string bounceSound = "FX_RicochetSound.Ricochet_Legacy";
private static readonly ConcurrentDictionary<uint, ImpactBudget> impactBudgets = [];
private static readonly HashSet<uint> ownedTracers = [];
@ -57,18 +58,10 @@ namespace src.player.skills
if (PlayerManager.GetPlayerByIndex(player.Index)?.Skill != skillName) return;
if (!RayTrace.IsAvailable)
{
Trace("RayTrace module unavailable");
return;
}
if (!RayTrace.IsAvailable) return;
var pawn = player.PlayerPawn?.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE)
{
Trace("shooter pawn invalid or dead");
return;
}
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null || pawn.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
if (!TryConsumeBudget(player.Index)) return;
@ -76,32 +69,14 @@ namespace src.player.skills
Vector3 impact = new(@event.X, @event.Y, @event.Z);
Vector3 toImpact = impact - eyePos;
if (toImpact.LengthSquared() < 1f)
{
Trace("impact too close to eye");
return;
}
if (toImpact.LengthSquared() < 1f) return;
Vector3 direction = Vector3.Normalize(toImpact);
var surface = RayTrace.TraceShape(player, ToVector(eyePos), ToVector(impact + direction * 8f), worldOnlyMask, 0);
if (surface == null)
{
Trace("surface trace returned null");
return;
}
if (surface == null || !surface.Value.DidHit) return;
if (!surface.Value.DidHit)
{
Trace($"surface trace missed (fraction={surface.Value.Fraction:0.###})");
return;
}
if (!TryResolveNormal(player, surface.Value, direction, out Vector3 normal))
{
Trace("could not resolve surface normal");
return;
}
if (!TryResolveNormal(player, surface.Value, direction, out Vector3 normal)) return;
Bounce(player, pawn, surface.Value.EndPos, direction, normal);
}
@ -118,31 +93,28 @@ namespace src.player.skills
KillfeedIcons? killfeedIcon = KillfeedIconsExtensions.FromWeaponName(SkillUtils.GetDesignerName(pawn.WeaponServices?.ActiveWeapon?.Value));
List<Vector3> path = [];
List<int> soundAt = [];
for (int i = 0; i < bounces; i++)
{
direction = Vector3.Reflect(direction, normal);
Vector3 start = point + normal * 2f;
if (path.Count == 0) path.Add(start);
if (path.Count == 0)
{
path.Add(start);
soundAt.Add(0);
}
Vector3 end = start + direction * segmentDistance;
var hit = RayTrace.TraceShape(player, ToVector(start), ToVector(end));
if (hit == null)
{
Trace($"bounce {i + 1}: segment trace returned null");
break;
}
if (hit == null) break;
Vector3 stop = hit.Value.DidHit ? hit.Value.EndPos : end;
path.Add(stop);
if (!hit.Value.DidHit)
{
Trace($"bounce {i + 1}: no surface, len={Vector3.Distance(start, stop):0.#}");
break;
}
if (!hit.Value.DidHit) break;
if (hit.Value.HitPlayer(out var victim))
{
@ -150,17 +122,14 @@ namespace src.player.skills
break;
}
if (!TryResolveNormal(player, hit.Value, direction, out normal))
{
Trace($"bounce {i + 1}: could not resolve normal");
break;
}
if (!TryResolveNormal(player, hit.Value, direction, out normal)) break;
soundAt.Add(path.Count - 1);
point = hit.Value.EndPos;
damage *= damageFalloff;
}
StartFlight(player, path);
StartFlight(player, path, soundAt);
}
public static void OnTick()
@ -191,11 +160,12 @@ namespace src.player.skills
float tail = MathF.Max(head - trail, flight.Cumulative[segment]);
UpdateTracer(flight, PointAt(flight, tail), PointAt(flight, head));
EmitBounceSound(flight, tail);
}
}
}
private static void StartFlight(CCSPlayerController player, List<Vector3> path)
private static void StartFlight(CCSPlayerController player, List<Vector3> path, List<int> soundAt)
{
if (path.Count < 2) return;
if (!SkillsInfo.GetValue<bool>(skillName, "showTracer")) return;
@ -223,10 +193,27 @@ namespace src.player.skills
Cumulative = cumulative,
TotalLength = total,
DeadlineTick = Server.TickCount + (int)(total / speed * 64f) + 128,
SoundAt = [.. soundAt.Where(index => index < points.Length)],
});
}
}
private static void EmitBounceSound(Flight flight, float tail)
{
if (flight.NextSound >= flight.SoundAt.Length) return;
if (tail < flight.Cumulative[flight.SoundAt[flight.NextSound]]) return;
while (flight.NextSound < flight.SoundAt.Length && tail >= flight.Cumulative[flight.SoundAt[flight.NextSound]])
flight.NextSound++;
if (flight.BeamIndex == null) return;
var beam = ResolveTracer(flight.BeamIndex.Value);
if (beam == null) return;
beam.EmitSound(bounceSound, volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
private static int SegmentAt(Flight flight, float distance)
{
for (int i = flight.Points.Length - 2; i >= 0; i--)
@ -332,9 +319,8 @@ namespace src.player.skills
beam.DispatchSpawn();
return beam;
}
catch (Exception ex)
catch
{
Trace($"beam create failed: {ex.Message}");
return null;
}
}
@ -352,7 +338,6 @@ namespace src.player.skills
if (beam == null)
{
ownedTracers.Remove(beamIndex);
Trace($"tracer {beamIndex} vanished before release");
return;
}
@ -395,9 +380,6 @@ namespace src.player.skills
if (beam != null) HideTracer(beam);
}
if (ownedTracers.Count > 0)
Trace($"pool reset: {ownedTracers.Count} tracer(s) released");
tracerPool.Clear();
ownedTracers.Clear();
}
@ -454,7 +436,6 @@ namespace src.player.skills
int finalDamage = (int)MathF.Round(damage);
if (finalDamage <= 0) return;
Trace($"hit {victimEvent.PlayerName} for {finalDamage}");
SkillUtils.TakeHealth(victimPawn, finalDamage, attacker, killfeedIcon ?? KillfeedIcons.Leg);
}
@ -466,7 +447,6 @@ namespace src.player.skills
if (TryRefineNormal(player, result.EndPos, direction, rough, out normal)) return true;
Trace("surface refine failed, using rough normal");
normal = rough;
return true;
}
@ -600,11 +580,6 @@ namespace src.player.skills
return Vector3.Distance(point, baseHit) <= tolerance;
}
private static void Trace(string message)
{
Debug.WriteToDebug($"[Ricochet] {message}");
}
private static bool TryConsumeBudget(uint playerIndex)
{
int tick = Server.TickCount;
@ -637,12 +612,15 @@ namespace src.player.skills
public required float[] Cumulative { get; set; }
public required float TotalLength { get; set; }
public required int DeadlineTick { get; set; }
public required int[] SoundAt { get; set; }
public float Travelled { get; set; }
public int NextSound { get; set; }
public uint? BeamIndex { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ffd75e", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Rare, int bounces = 3, float segmentDistance = 1200f, float damageMultiplier = .5f, float fallbackDamage = 25f, bool respectArmor = true, float damageFalloff = .75f, int maxImpactsPerTick = 2, bool showTracer = true, float tracerWidth = 0.5f, float tracerSpeed = 2600f, float tracerLength = 220f, int maxActiveTracers = 12, float normalProbeOffset = 6f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ffd75e", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Rare, int bounces = 3, float segmentDistance = 1200f, float damageMultiplier = .5f, float fallbackDamage = 25f, bool respectArmor = true, float damageFalloff = .75f, int maxImpactsPerTick = 2, bool showTracer = true, float tracerWidth = 0.5f, float tracerSpeed = 2600f, float tracerLength = 220f, int maxActiveTracers = 12, float normalProbeOffset = 6f, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int Bounces { get; set; } = bounces;
public float SegmentDistance { get; set; } = segmentDistance;
public float DamageMultiplier { get; set; } = damageMultiplier;

View file

@ -30,6 +30,8 @@ namespace src.player.skills
if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim) return;
var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index);
if (Heavyweight.Resists(victim)) return;
var victimPawn = victim!.PlayerPawn.Value;
if (victimPawn == null || !victimPawn.IsValid) return;

View file

@ -16,13 +16,7 @@ namespace src.player.skills
private static readonly object setLock = new();
private const string weapon_awp = "weapon_awp";
private static readonly string[] rifles =
[
"weapon_mp9", "weapon_mac10", "weapon_bizon", "weapon_mp7", "weapon_ump45", "weapon_p90",
"weapon_mp5sd", "weapon_famas", "weapon_galilar", "weapon_m4a1", "weapon_m4a1_silencer", "weapon_ak47",
"weapon_aug", "weapon_sg556", "weapon_ssg08", "weapon_awp", "weapon_scar20", "weapon_g3sg1",
"weapon_nova", "weapon_xm1014", "weapon_mag7", "weapon_sawedoff", "weapon_m249", "weapon_negev"
];
private static HashSet<string> rifles => WeaponPool.Rifles;
public static void LoadSkill()
{
@ -81,7 +75,7 @@ namespace src.player.skills
foreach (var index in AWPs.ToList())
SkillUtils.SafeKillEntity<CBasePlayerWeapon>(index);
if (savedWeapons.TryGetValue(player.Index, out string? savedWeapon) && !string.IsNullOrWhiteSpace(savedWeapon))
if (!SkillUtils.IsPistolRound() && savedWeapons.TryGetValue(player.Index, out string? savedWeapon) && !string.IsNullOrWhiteSpace(savedWeapon))
Server.NextFrame(() =>
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;

View file

@ -100,9 +100,13 @@ namespace src.player.skills
{
if (SkillPlayerInfo.IsEmpty) return;
if (Server.TickCount % 60 != 0) return;
int cooldown = SkillsInfo.GetValue<int>(skillName, "cooldown");
if (cooldown < 1) cooldown = 1;
if (Server.TickCount % (60 * cooldown) != 0) return;
if ((Server.TickCount / 60) % cooldown != 0) return;
float volume = SkillsInfo.GetValue<float>(skillName, "soundVolume");
foreach (var player in PlayerManager.GetTickPlayers()
.Where(p => p != null && p.IsValid && p.PlayerPawn.Value != null && p.PlayerPawn.Value.IsValid && p.PlayerPawn.Value.Health > 0))
@ -111,18 +115,19 @@ namespace src.player.skills
if (entities.Count == 0)
{
player.PlayerPawn.Value!.EmitSound(soundEventName, volume: 1f);
player.PlayerPawn.Value!.EmitSound(soundEventName, volume: volume);
continue;
}
var entity = Utilities.GetEntityFromIndex<CDynamicProp>((int)entities[0]);
if (entity != null && entity.IsValid)
entity.EmitSound(soundEventName, volume: 1f);
entity.EmitSound(soundEventName, volume: volume);
}
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#e3ed8c", 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 cooldown = 2) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#e3ed8c", 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 cooldown = 2, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int Cooldown { get; set; } = cooldown;
}
}

View file

@ -167,7 +167,7 @@ namespace src.player.skills
SetMagazines(player, playerMagazines + enemyMagazines);
SetMagazines(enemy, 0);
PlayerManager.GetPlayerFromEvent(enemy)?.ExecuteClientCommand($"play sounds/weapons/g3sg1/g3sg1_clipout");
player.PlayerPawn?.Value?.EmitSound("BaseCombatCharacter.AmmoPickup", volume: SkillsInfo.GetValue<float>(skillName, "soundVolume"));
var enemyEvent = PlayerManager.GetPlayerFromEvent(enemy);
if (enemyEvent == null || !enemyEvent.IsValid) return;
@ -299,8 +299,9 @@ namespace src.player.skills
public required int Reserve { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5eb8b0", 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, float cooldown = 30f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5eb8b0", 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, float cooldown = 30f, float soundVolume = 1f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float Cooldown { get; set; } = cooldown;
}
}

View file

@ -40,12 +40,13 @@ namespace src.player.skills
damage = Math.Min(damage, SkillsInfo.GetValue<int>(skillName, "maxTakenDamagePerShot"));
SkillUtils.TakeHealth(attackerEvent.PlayerPawn.Value, damage, victimEvent, KillfeedIcons.Armor);
attackerEvent.EmitSound("Player.DamageBody.Onlooker");
SkillUtils.EmitSoundToPlayer(attackerEvent, "Player.DamageBody.Victim", SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#962631", 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, float healthTakenScale = .3f, int maxTakenDamagePerShot = 37) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#962631", 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, float healthTakenScale = .3f, int maxTakenDamagePerShot = 37, float soundVolume = .35f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float HealthTakenScale { get; set; } = healthTakenScale;
public int MaxTakenDamagePerShot { get; set; } = maxTakenDamagePerShot;
}

View file

@ -105,7 +105,7 @@ namespace src.player.skills
}
if (SkillUtils.TakeHealth(pawn, damage, thrower, KillfeedIcons.Smokegrenade))
player.EmitSound("Player.DamageBody.Onlooker");
SkillUtils.EmitSoundToPlayer(eventPlayer, "Player.DamageBody.Victim", SkillsInfo.GetValue<float>(skillName, "soundVolume"));
}
}
}
@ -170,8 +170,9 @@ namespace src.player.skills
SkillUtils.UpdateGrenadeCount(player, CsItem.SmokeGrenade, 1);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#507529", 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 smokeDamage = 2, float smokeRadius = 180, int tickCooldown = 17, int grenadeLimit = 1, float dmgReductionForTeamates = 0.5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#507529", 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 smokeDamage = 2, float smokeRadius = 180, int tickCooldown = 17, int grenadeLimit = 1, float dmgReductionForTeamates = 0.5f, float soundVolume = .3f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int SmokeDamage { get; set; } = smokeDamage;
public float SmokeRadius { get; set; } = smokeRadius;
public int TickCooldown { get; set; } = tickCooldown;

View file

@ -12,6 +12,7 @@ namespace src.player.skills
public class Tripwire : ISkill
{
private const Skills skillName = Skills.Tripwire;
private const string triggerSound = "Weapon_Taser.ChargeReady";
private static readonly ConcurrentDictionary<uint, WireInfo> wires = [];
private static readonly ConcurrentDictionary<uint, PlayerSkillInfo> SkillPlayerInfo = [];
@ -267,7 +268,7 @@ namespace src.player.skills
var ownerEvent = PlayerManager.GetPlayerFromEvent(owner);
if (ownerEvent != null && ownerEvent.IsValid)
{
ownerEvent.ExecuteClientCommand("play sounds/weapons/taser/taser_charge_ready");
SkillUtils.EmitSoundToPlayer(ownerEvent, triggerSound, SkillsInfo.GetValue<float>(skillName, "soundVolume"));
ownerEvent.PrintToChat($" {ChatColors.Red}" + ownerEvent.GetTranslation("tripwire_triggered_info", enemyEvent.PlayerName));
}
}
@ -314,8 +315,9 @@ namespace src.player.skills
public DateTime LastAttempt { get; set; }
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff3b3b", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, float radarDuration = 5f, float triggerRadius = 24f, float wireHeight = 30f, float wireWidth = 0.7f, float maxWallDistance = 400f, float cooldown = 20f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff3b3b", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Rare, float radarDuration = 5f, float triggerRadius = 24f, float wireHeight = 30f, float wireWidth = 0.7f, float maxWallDistance = 400f, float cooldown = 20f, float soundVolume = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public float RadarDuration { get; set; } = radarDuration;
public float TriggerRadius { get; set; } = triggerRadius;
public float WireHeight { get; set; } = wireHeight;

View file

@ -0,0 +1,116 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using src.utils;
using System.Collections.Concurrent;
namespace src.player.skills
{
public class TrueArmor : ISkill
{
private const Skills skillName = Skills.TrueArmor;
private static readonly ConcurrentDictionary<uint, byte> holders = [];
private static readonly ConcurrentDictionary<uint, int> pending = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
pending.Clear();
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
holders[player.Index] = 0;
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
holders.TryRemove(player.Index, out _);
pending.TryRemove(player.Index, out _);
}
public static void PlayerDisconnect(uint playerIndex)
{
holders.TryRemove(playerIndex, out _);
pending.TryRemove(playerIndex, out _);
}
public static void OnTakeDamage(DynamicHook h)
{
if (holders.IsEmpty) return;
if (!TryResolveVictim(h, out var victim, out var victimPawn, out var info)) return;
if (!holders.ContainsKey(victim!.Index)) return;
int armor = victimPawn!.ArmorValue;
if (armor <= 0) return;
float damage = info!.Damage;
if (damage <= 0) return;
float ratio = SkillsInfo.GetValue<float>(skillName, "armorShare");
float absorbed = MathF.Min(damage * ratio, armor);
if (absorbed <= 0) return;
info.Damage = damage - absorbed;
pending[victim.Index] = (int)MathF.Round(armor - absorbed);
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}");
}
public static void OnTakeDamagePost(DynamicHook h)
{
if (pending.IsEmpty) return;
if (!TryResolveVictim(h, out var victim, out var victimPawn, out _)) return;
if (!pending.TryRemove(victim!.Index, out int restored)) return;
victimPawn!.ArmorValue = restored;
Utilities.SetStateChanged(victimPawn, "CCSPlayerPawn", "m_ArmorValue");
}
private static bool TryResolveVictim(DynamicHook h, out CCSPlayerController? victim, out CCSPlayerPawn? pawn, out CTakeDamageInfo? info)
{
victim = null;
pawn = null;
info = null;
CEntityInstance param = h.GetParam<CEntityInstance>(0);
CTakeDamageInfo param2 = h.GetParam<CTakeDamageInfo>(1);
if (param == null || !param.IsValid || param2 == null) return false;
var victimPawn = param.As<CCSPlayerPawn>();
if (victimPawn == null || !victimPawn.IsValid || victimPawn.DesignerName != "player") return false;
var controller = victimPawn.Controller.Value;
if (controller == null || !controller.IsValid) return false;
var resolved = controller.As<CCSPlayerController>();
if (resolved == null || !resolved.IsValid) return false;
var routed = PlayerManager.GetPlayerEvent(resolved);
victim = routed != null && routed.IsValid ? routed : resolved;
pawn = victimPawn;
info = param2;
return true;
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#9ad3f5", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Uncommon, float armorShare = .33f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float ArmorShare { get; set; } = armorShare;
}
}
}

View file

@ -0,0 +1,157 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using jRandomSkills.src.utils;
using src.utils;
using System.Collections.Concurrent;
namespace src.player.skills
{
public class Voodoo : ISkill
{
private const Skills skillName = Skills.Voodoo;
private static readonly ConcurrentDictionary<uint, uint> ownerToVictim = [];
private static readonly ConcurrentDictionary<uint, byte> reflecting = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
ownerToVictim.Clear();
reflecting.Clear();
foreach (var player in PlayerManager.GetTickPlayers())
SkillUtils.CloseMenu(player);
}
public static void PlayerDisconnect(uint playerIndex)
{
ownerToVictim.TryRemove(playerIndex, out _);
reflecting.TryRemove(playerIndex, out _);
foreach (var kvp in ownerToVictim)
if (kvp.Value == playerIndex)
ownerToVictim.TryRemove(kvp.Key, out _);
}
public static void EnableSkill(CCSPlayerController player)
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
playerInfo.SkillUsed = false;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
var enemies = SkillUtils.GetSelectableEnemies(player, true);
if (enemies.Length > 0)
{
ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString())));
SkillUtils.CreateMenu(player, menuItems);
}
else
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
ownerToVictim.TryRemove(player.Index, out _);
SkillUtils.CloseMenu(player);
}
public static void OnTick()
{
if (Server.TickCount % 32 != 0) return;
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null || playerInfo.Skill != skillName) continue;
if (!SkillUtils.HasMenu(player)) continue;
var enemies = SkillUtils.GetSelectableEnemies(player, true);
ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString())));
SkillUtils.UpdateMenu(player, menuItems);
}
}
public static void TypeSkill(CCSPlayerController player, string[] commands)
{
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return;
var playerEvent = PlayerManager.GetPlayerFromEvent(player);
if (playerEvent == null || !playerEvent.IsValid) return;
if (playerInfo.SkillUsed)
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("areareaper_used_info")}");
return;
}
if (commands.Length == 0 || !uint.TryParse(commands[0], out uint enemyIndex))
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
return;
}
var enemy = Utilities.GetPlayerFromIndex((int)enemyIndex);
if (enemy == null || !enemy.IsValid)
{
playerEvent.PrintToChat($" {ChatColors.Red}{playerEvent.GetTranslation("selectplayerskill_incorrect_enemy_index")}");
return;
}
ownerToVictim[player.Index] = enemy.Index;
playerInfo.SkillUsed = true;
playerEvent.PrintToChat($" {ChatColors.Green}{playerEvent.GetTranslation("voodoo_player_info", enemy.PlayerName)}");
}
public static void PlayerHurt(EventPlayerHurt @event)
{
if (ownerToVictim.IsEmpty) return;
var owner = PlayerManager.GetPlayerEvent(@event.Userid);
if (owner == null || !owner.IsValid) return;
if (!ownerToVictim.TryGetValue(owner.Index, out uint victimIndex)) return;
if (@event.DmgHealth <= 0) return;
if (!reflecting.TryAdd(owner.Index, 0)) return;
try
{
var victim = Utilities.GetPlayerFromIndex((int)victimIndex);
if (victim == null || !victim.IsValid) return;
var victimPawn = victim.PlayerPawn?.Value;
if (victimPawn == null || !victimPawn.IsValid || victimPawn.Health <= 0) return;
if (victimPawn.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
int reflected = (int)MathF.Round(@event.DmgHealth * SkillsInfo.GetValue<float>(skillName, "reflectPercent"));
if (reflected < 1) return;
SkillUtils.TakeHealth(victimPawn, reflected, owner, KillfeedIcons.Fist);
}
finally
{
reflecting.TryRemove(owner.Index, out _);
}
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#7b2d8b", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 2, Rarity rarity = Rarity.Rare, float reflectPercent = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float ReflectPercent { get; set; } = reflectPercent;
}
}
}

View file

@ -50,15 +50,16 @@ namespace src.player.skills
else
Localization.PrintTranslationToChatAll($" {ChatColors.LightBlue}{{0}}", ["watchmaker_ct"], true, [roundTime]);
player.EmitSound(SkillsInfo.GetValue<string>(skillName, "SoundEvent"));
SkillUtils.EmitSoundToPlayer(player, SkillsInfo.GetValue<string>(skillName, "SoundEvent"), SkillsInfo.GetValue<float>(skillName, "soundVolume"));
var proxy = Utilities.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules").FirstOrDefault();
if (proxy == null) return;
Utilities.SetStateChanged(proxy, "CCSGameRulesProxy", "m_pGameRules");
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff462e", 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 changeRoundTime = 7, string soundEvent = "UIPanorama.sidemenu_select") : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff462e", 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 changeRoundTime = 7, string soundEvent = "UIPanorama.sidemenu_select", float soundVolume = .5f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float SoundVolume { get; set; } = soundVolume;
public int ChangeRoundTime { get; set; } = changeRoundTime;
public string SoundEvent { get; set; } = soundEvent;
}

View file

@ -13,12 +13,7 @@ namespace src.player.skills
private static readonly ConcurrentDictionary<uint, PlayerSkillInfo> SkillPlayerInfo = [];
private static readonly object setLock = new();
private static readonly HashSet<string> weapons = [ "weapon_deagle", "weapon_revolver", "weapon_glock", "weapon_usp_silencer",
"weapon_cz75a", "weapon_fiveseven", "weapon_p250", "weapon_tec9", "weapon_elite", "weapon_hkp2000",
"weapon_mp9", "weapon_mac10", "weapon_bizon", "weapon_mp7", "weapon_ump45", "weapon_p90", "weapon_sg556",
"weapon_mp5sd", "weapon_famas", "weapon_galilar", "weapon_m4a4", "weapon_m4a1_silencer", "weapon_ak47",
"weapon_aug", "weapon_sg556", "weapon_ssg08", "weapon_awp", "weapon_scar20", "weapon_g3sg1",
"weapon_nova", "weapon_xm1014", "weapon_mag7", "weapon_sawedoff", "weapon_m249", "weapon_negev" ];
private static HashSet<string> weapons => WeaponPool.All;
public static void LoadSkill()
{