Fix leaked skill state, second chance revives, and unrestored server cvars
General: - Jester's no-damage state no longer applies to players who do not hold the skill. The shared health helper checked Jester's internal table without verifying the current assignment, so a single stale entry silently cancelled damage across every skill that deals damage through it. - Jester's per-round reset no longer aborts when the holder is dead, which previously left the table populated for the following rounds. - The per-round reset now covers every skill used on the current map instead of only the ones held in the round that just ended, so a skill nobody drew still clears state left over from an earlier round. - Skills that track another player as a target are now notified when that player disconnects. Controller indexes are reused, so a leftover entry applied to whoever connected into that index next. Affects Poison, Deaf, Darkness, Glitch, Magnifier, Legless, No Rifles, Jammer and Jester. - Aimbot no longer retains raw native pointers after a hit is handled. They were never cleared and were written back to later, which could corrupt memory the engine had already freed. - An exception inside a skill's OnTick no longer cancels the tick of every skill after it in the same frame. - Cooldowns configured low enough to truncate to zero no longer throw (Poison, Regeneration, Hot Bomb, Healing Smoke, Toxic Smoke). - Round changes are cheaper: Long Knife no longer detaches a detour it never attached, and Wild Throw skips its player sweep when it has nothing to unwind. Skill improvements: - Second Chance: now survives several lethal hits landing in the same tick (shotgun, two attackers) and restores health inside the damage hook instead of a frame later. It also runs after skills that modify damage, so One-Shot and Soldier are seen at their final value instead of the raw one. - Spectator: returning to your own view no longer leaves the camera handle set, which made the tick handler reset the weapon cooldown every frame and play as rapid fire. The manual toggle is unchanged. - Radar Hack: players hidden behind a replacement model (Chicken) are shown on the radar again. Only Ghost, Ninja and C4 Camouflage hide the pawn itself, so render alpha alone no longer decides. - Focus: weapon_accuracy_nospread is reference counted, so one holder losing the skill no longer removes the effect from the others still holding it. - Friendly Fire: mp_autokick is restored at the end of the round instead of staying disabled for the rest of the map, and is no longer executed on every friendly hit. - Reactive Armor / Iana: no longer restore health for a player who no longer holds the skill. - Free Planter / Short Fuse: each restores mp_c4timer only when it actually changed it, so overlapping rounds cannot write back the wrong value. - Illusionist: the replica movement timer now stops on map change. - Ghost / Ninja / C4 Camouflage: the bomb is located once per transmit instead of once per receiver and hidden player, and the pass is skipped when nobody is hidden.
This commit is contained in:
parent
92175f6949
commit
21e702654d
34 changed files with 316 additions and 102 deletions
|
|
@ -26,6 +26,7 @@ namespace src
|
|||
public IWasdMenuManager? MenuManager;
|
||||
// Skills that were enabled at least once this round; used to reset only those on round change (not all 124).
|
||||
public static readonly ConcurrentDictionary<string, byte> ActiveSkillsThisRound = new();
|
||||
public static readonly ConcurrentDictionary<string, byte> SkillsUsedThisMap = new();
|
||||
|
||||
public override string ModuleName => "[CS2] [ jRandomSkills ]";
|
||||
public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)";
|
||||
|
|
@ -95,7 +96,10 @@ namespace src
|
|||
return null;
|
||||
|
||||
if (methodName == "EnableSkill")
|
||||
{
|
||||
ActiveSkillsThisRound.TryAdd(skill, 0);
|
||||
SkillsUsedThisMap.TryAdd(skill, 0);
|
||||
}
|
||||
|
||||
var method = _skillMethodCache.GetOrAdd((skill, methodName), key =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -138,24 +138,56 @@ namespace src.player
|
|||
return candidates[Random.Shared.Next(candidates.Count)];
|
||||
}
|
||||
|
||||
private static readonly Skills[] lateDamageSkills = [Skills.SecondLife];
|
||||
|
||||
private static readonly HashSet<Skills> tickFailuresLogged = [];
|
||||
|
||||
private static void InvokeSkill(Skills skill, string methodName, object[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Instance.SkillAction(skill.ToString(), methodName, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Server.PrintToConsole($"[jRandomSkills] {skill}.{methodName} failed: {ex.InnerException?.Message ?? ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void DispatchToActiveSkills(string methodName, params object[] args)
|
||||
{
|
||||
var seen = new HashSet<Skills>();
|
||||
foreach (var p in Instance.SkillPlayer)
|
||||
{
|
||||
if (p.IsDrawing || !seen.Add(p.Skill)) continue;
|
||||
try
|
||||
{
|
||||
Instance.SkillAction(p.Skill.ToString(), methodName, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// One skill's failure must not break the dispatch chain.
|
||||
Server.PrintToConsole($"[jRandomSkills] {p.Skill}.{methodName} failed: {ex.InnerException?.Message ?? ex.Message}");
|
||||
}
|
||||
InvokeSkill(p.Skill, methodName, args);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DispatchOnTakeDamage(DynamicHook h)
|
||||
{
|
||||
object[] args = [h];
|
||||
var seen = new HashSet<Skills>();
|
||||
List<Skills>? deferred = null;
|
||||
|
||||
foreach (var p in Instance.SkillPlayer)
|
||||
{
|
||||
if (p.IsDrawing || !seen.Add(p.Skill)) continue;
|
||||
|
||||
if (Array.IndexOf(lateDamageSkills, p.Skill) >= 0)
|
||||
{
|
||||
(deferred ??= []).Add(p.Skill);
|
||||
continue;
|
||||
}
|
||||
|
||||
InvokeSkill(p.Skill, "OnTakeDamage", args);
|
||||
}
|
||||
|
||||
if (deferred == null) return;
|
||||
foreach (var skill in deferred)
|
||||
InvokeSkill(skill, "OnTakeDamage", args);
|
||||
}
|
||||
|
||||
private static HookResult PlayerMakeSound(UserMessage um)
|
||||
{
|
||||
lock (setLock)
|
||||
|
|
@ -322,7 +354,7 @@ namespace src.player
|
|||
{
|
||||
lock (setLock)
|
||||
{
|
||||
DispatchToActiveSkills("OnTakeDamage", h);
|
||||
DispatchOnTakeDamage(h);
|
||||
|
||||
if (Fortnite.skillInThisRound == true &&
|
||||
!Instance.SkillPlayer.Any(p => !p.IsDrawing && p.Skill == Skills.Fortnite))
|
||||
|
|
@ -468,7 +500,17 @@ namespace src.player
|
|||
foreach (var skill in _activeSkillsList)
|
||||
{
|
||||
if (freeze && _freezeDisabledSkills.Contains(skill)) continue;
|
||||
Instance.SkillAction(_skillNames[skill], "OnTick");
|
||||
try
|
||||
{
|
||||
Instance.SkillAction(_skillNames[skill], "OnTick");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Without this one throwing skill cancels every later skill's tick, every frame.
|
||||
// Logged once per skill per round; at 64 ticks a repeat would flood the console.
|
||||
if (tickFailuresLogged.Add(skill))
|
||||
Server.PrintToConsole($"[jRandomSkills] {skill}.OnTick failed: {ex.InnerException?.Message ?? ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
PerfLog.Sample("OnTick(skills)", perfStart);
|
||||
|
|
@ -541,6 +583,10 @@ namespace src.player
|
|||
|
||||
Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [player]);
|
||||
|
||||
uint leavingIndex = player.Index;
|
||||
foreach (var skill in SkillData.Skills)
|
||||
Instance.SkillAction(skill.Skill.ToString(), "PlayerDisconnect", [leavingIndex]);
|
||||
|
||||
PlayerManager.UnregisterPlayer(player.Index);
|
||||
EntityManager.DestroyPlayerEntities(player.Index);
|
||||
|
||||
|
|
@ -660,8 +706,12 @@ namespace src.player
|
|||
if (playerInfo == null) continue;
|
||||
|
||||
ActiveSkillsThisRound.TryAdd(playerInfo.Skill.ToString(), 0);
|
||||
SkillsUsedThisMap.TryAdd(playerInfo.Skill.ToString(), 0);
|
||||
if (playerInfo.SpecialSkill != noneSkill.Skill)
|
||||
{
|
||||
ActiveSkillsThisRound.TryAdd(playerInfo.SpecialSkill.ToString(), 0);
|
||||
SkillsUsedThisMap.TryAdd(playerInfo.SpecialSkill.ToString(), 0);
|
||||
}
|
||||
|
||||
Instance.SkillAction(playerInfo.Skill.ToString(), "DisableSkill", [player]);
|
||||
|
||||
|
|
@ -674,9 +724,13 @@ namespace src.player
|
|||
RestorePlayer(player);
|
||||
}
|
||||
|
||||
foreach (var skillName in ActiveSkillsThisRound.Keys)
|
||||
// Reset every skill used so far on this map, not only the ones held this round: a skill
|
||||
// nobody drew now would otherwise never clear state left over from an earlier round.
|
||||
// Skills that never ran cannot hold state, so they stay out of the sweep.
|
||||
foreach (var skillName in SkillsUsedThisMap.Keys)
|
||||
Instance.SkillAction(skillName, "NewRound");
|
||||
ActiveSkillsThisRound.Clear();
|
||||
tickFailuresLogged.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -712,6 +766,7 @@ namespace src.player
|
|||
EntityManager.SuppressKills = false;
|
||||
|
||||
ActiveSkillsThisRound.Clear();
|
||||
SkillsUsedThisMap.Clear();
|
||||
nextRoundPicks.Clear();
|
||||
|
||||
playersSkills.Clear();
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ namespace src.player.skills
|
|||
Marshal.WriteInt32(hitGroupOffset, 56, hitGroup);
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
hitGroups.Clear();
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController _)
|
||||
{
|
||||
foreach (var hit in hitGroups)
|
||||
|
|
@ -65,6 +70,8 @@ namespace src.player.skills
|
|||
if (hit.Key != nint.Zero)
|
||||
Marshal.WriteInt32(hit.Key, 56, hit.Value);
|
||||
}
|
||||
|
||||
hitGroups.Clear();
|
||||
}
|
||||
|
||||
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff0000", 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) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ namespace src.player.skills
|
|||
{
|
||||
if (Server.TickCount % 2 != 0) return;
|
||||
|
||||
var bomb = invisiblePlayers.IsEmpty
|
||||
? null
|
||||
: Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
|
||||
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
{
|
||||
if (player.PlayerPawn?.Value?.Health <= 0 && invisiblePlayers.ContainsKey(player.Index))
|
||||
|
|
@ -77,7 +81,7 @@ namespace src.player.skills
|
|||
|
||||
// CheckTransmit hides the model but the radar blip comes from spotted state, so clear it every tick.
|
||||
if (invisiblePlayers.ContainsKey(player.Index))
|
||||
ClearSpottedState(player);
|
||||
ClearSpottedState(player, bomb);
|
||||
|
||||
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
|
||||
if (playerInfo?.Skill != skillName) continue;
|
||||
|
|
@ -99,6 +103,11 @@ namespace src.player.skills
|
|||
|
||||
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
|
||||
{
|
||||
if (invisiblePlayers.IsEmpty) return;
|
||||
|
||||
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
|
||||
if (bomb != null && !bomb.IsValid) bomb = null;
|
||||
|
||||
foreach (var (info, player) in infoList)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
|
||||
|
|
@ -127,14 +136,10 @@ namespace src.player.skills
|
|||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
|
||||
var bombIndex = GetBombIndex();
|
||||
if (bombIndex == null) continue;
|
||||
if (bomb == null) continue;
|
||||
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
if (info.TransmitEntities.Contains(bomb.Index))
|
||||
info.TransmitEntities.Remove(bomb.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -212,19 +217,8 @@ namespace src.player.skills
|
|||
particle.AcceptInput("Start");
|
||||
}
|
||||
|
||||
private static uint? GetBombIndex()
|
||||
{
|
||||
var bombEntities = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").ToList();
|
||||
if (bombEntities.Count == 0) return null;
|
||||
|
||||
var bomb = bombEntities.FirstOrDefault();
|
||||
if (bomb == null) return null;
|
||||
|
||||
return bomb.Index;
|
||||
}
|
||||
|
||||
// Wipe spotted state (pawn + carried bomb) so a disguised carrier produces no radar blip.
|
||||
private static void ClearSpottedState(CCSPlayerController player)
|
||||
private static void ClearSpottedState(CCSPlayerController player, CC4? bomb)
|
||||
{
|
||||
var pawn = player.PlayerPawn?.Value;
|
||||
if (pawn != null && pawn.IsValid)
|
||||
|
|
@ -234,7 +228,6 @@ namespace src.player.skills
|
|||
pawn.EntitySpottedState.SpottedByMask[1] = 0;
|
||||
}
|
||||
|
||||
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
|
||||
if (bomb != null && bomb.IsValid && bomb.OwnerEntity?.Index == player.Index)
|
||||
{
|
||||
bomb.EntitySpottedState.Spotted = false;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,19 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
lock (setLock)
|
||||
{
|
||||
playersInDark.TryRemove(playerIndex, out _);
|
||||
playersToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in playersToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
playersToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
lock (setLock)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
deafPlayers.TryRemove(playerIndex, out _);
|
||||
playersToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in playersToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
playersToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
foreach (var playerIndex in deafPlayers.Keys)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using src.utils;
|
||||
|
|
@ -9,10 +10,23 @@ namespace src.player.skills
|
|||
public class FriendlyFire : ISkill
|
||||
{
|
||||
private const Skills skillName = Skills.FriendlyFire;
|
||||
private static bool defaultAutoKick = true;
|
||||
private static bool autoKickOverridden;
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
|
||||
try { defaultAutoKick = ConVar.Find("mp_autokick")?.GetPrimitiveValue<bool>() ?? true; }
|
||||
catch { defaultAutoKick = true; }
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
if (!autoKickOverridden) return;
|
||||
|
||||
autoKickOverridden = false;
|
||||
Server.ExecuteCommand($"mp_autokick {(defaultAutoKick ? 1 : 0)}");
|
||||
}
|
||||
|
||||
public static void OnTakeDamage(DynamicHook h)
|
||||
|
|
@ -56,8 +70,12 @@ namespace src.player.skills
|
|||
float damage = param2.Damage;
|
||||
param2.Damage = 0;
|
||||
|
||||
Server.ExecuteCommand("mp_autokick 0");
|
||||
|
||||
if (!autoKickOverridden)
|
||||
{
|
||||
autoKickOverridden = true;
|
||||
Server.ExecuteCommand("mp_autokick 0");
|
||||
}
|
||||
|
||||
SkillUtils.AddHealth(
|
||||
victimPawn,
|
||||
(int)(damage * SkillsInfo.GetValue<float>(skillName, "healthDamageMultiplier")),
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ namespace src.player.skills
|
|||
|
||||
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
|
||||
{
|
||||
if (invisiblePlayers.IsEmpty) return;
|
||||
|
||||
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
|
||||
uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null;
|
||||
|
||||
foreach (var (info, player) in infoList)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
|
||||
|
|
@ -84,14 +89,11 @@ namespace src.player.skills
|
|||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
|
||||
var bombIndex = GetBombIndex(playerController);
|
||||
if (bombIndex == null) continue;
|
||||
// Hide the bomb as well, but only while this hidden player is the one holding it.
|
||||
if (bomb == null || bombOwnerIndex != playerController.Index) continue;
|
||||
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
if (info.TransmitEntities.Contains(bomb.Index))
|
||||
info.TransmitEntities.Remove(bomb.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -232,18 +234,6 @@ namespace src.player.skills
|
|||
playerInfo.PrintHTML = $"<font color='#FF0000'>{player.GetTranslation("disabled_weapon")}</font>";
|
||||
}
|
||||
|
||||
private static uint? GetBombIndex(CCSPlayerController player)
|
||||
{
|
||||
var bombEntities = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").ToList();
|
||||
if (bombEntities.Count == 0) return null;
|
||||
|
||||
var bomb = bombEntities.FirstOrDefault();
|
||||
if (bomb == null) return null;
|
||||
|
||||
if (bomb.OwnerEntity.Index != player.Index) return null;
|
||||
return bomb.Index;
|
||||
}
|
||||
|
||||
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#FFFFFF", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Epic) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,19 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
lock (setLock)
|
||||
{
|
||||
glitchedPlayers.TryRemove(playerIndex, out _);
|
||||
playersToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in playersToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
playersToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
lock (setLock)
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ namespace src.player.skills
|
|||
|
||||
public static void OnTick()
|
||||
{
|
||||
int tick = SkillsInfo.GetValue<int>(skillName, "tickCooldown");
|
||||
int tick = Math.Max(1, SkillsInfo.GetValue<int>(skillName, "tickCooldown"));
|
||||
if (Server.TickCount % tick != 0) return;
|
||||
|
||||
float smokeRadius = SkillsInfo.GetValue<float>(skillName, "smokeRadius");
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace src.player.skills
|
|||
|
||||
public static void OnTick()
|
||||
{
|
||||
int cooldown = (int)(SkillsInfo.GetValue<float>(skillName, "cooldown") * 64);
|
||||
int cooldown = Math.Max(1, (int)(SkillsInfo.GetValue<float>(skillName, "cooldown") * 64));
|
||||
if (Server.TickCount % cooldown != 0) return;
|
||||
|
||||
if (players.IsEmpty || jRandomSkills.Instance.GameRules?.FreezePeriod == true) return;
|
||||
|
|
|
|||
|
|
@ -340,6 +340,8 @@ namespace src.player.skills
|
|||
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
|
||||
if (victim == null || !victim.IsValid) return;
|
||||
|
||||
if (PlayerManager.GetPlayerByIndex(victim.Index)?.Skill != skillName) return;
|
||||
|
||||
if (playersInfo.TryGetValue(victim.Index, out var playerSkill))
|
||||
{
|
||||
if (playerSkill.CloneProp != null)
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ namespace src.player.skills
|
|||
replica.Teleport(nexPos, null, null);
|
||||
}, TimerFlags.REPEAT | TimerFlags.STOP_ON_MAPCHANGE);
|
||||
ActiveTimers.TryAdd(replicaIndex, moveTimer);
|
||||
});
|
||||
}, TimerFlags.STOP_ON_MAPCHANGE);
|
||||
|
||||
float duration = SkillsInfo.GetValue<float>(skillName, ducking ? "durationCrouch" : "durationRun");
|
||||
Instance.AddTimer(duration, () =>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,19 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"), false);
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
lock (setLock)
|
||||
{
|
||||
jammedPlayers.TryRemove(playerIndex, out _);
|
||||
jammerToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in jammerToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
jammerToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
lock (setLock)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
if (!jesters.TryRemove(playerIndex, out var jester)) return;
|
||||
|
||||
jester.Generation++;
|
||||
jester.Active = false;
|
||||
jester.Timer?.Kill();
|
||||
jester.Timer = null;
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
Server.NextWorldUpdate(() =>
|
||||
|
|
@ -37,7 +47,7 @@ namespace src.player.skills
|
|||
SkillUtils.ResetPrintHTML(player);
|
||||
|
||||
var pawn = player.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return;
|
||||
if (pawn == null || !pawn.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) continue;
|
||||
|
||||
var color = Color.FromArgb(255, 255, 255, 255);
|
||||
pawn.Render = color;
|
||||
|
|
@ -114,11 +124,10 @@ namespace src.player.skills
|
|||
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
|
||||
|
||||
if (!Instance.IsPlayerValid(victim)) return;
|
||||
var jesterVictim = GetJesterInfo(victim!.Index);
|
||||
|
||||
if (!Instance.IsPlayerValid(attacker))
|
||||
{
|
||||
if (jesterVictim != null && jesterVictim.Active)
|
||||
if (IsActiveJester(victim!.Index))
|
||||
{
|
||||
SkillUtils.RestoreHealth(victim);
|
||||
RestoreArmor(victim, @event.DmgArmor);
|
||||
|
|
@ -126,14 +135,19 @@ namespace src.player.skills
|
|||
return;
|
||||
}
|
||||
|
||||
var jesterAttacker = GetJesterInfo(attacker!.Index);
|
||||
if ((jesterVictim != null && jesterVictim.Active) || (jesterAttacker != null && jesterAttacker.Active))
|
||||
if (IsActiveJester(victim!.Index) || IsActiveJester(attacker!.Index))
|
||||
{
|
||||
SkillUtils.RestoreHealth(victim);
|
||||
RestoreArmor(victim, @event.DmgArmor);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsActiveJester(uint playerIndex)
|
||||
{
|
||||
if (GetJesterInfo(playerIndex)?.Active != true) return false;
|
||||
return PlayerManager.GetPlayerByIndex(playerIndex)?.Skill == skillName;
|
||||
}
|
||||
|
||||
private static void RestoreArmor(CCSPlayerController? victim, int dmgArmor)
|
||||
{
|
||||
if (victim == null || !victim.IsValid || dmgArmor <= 0) return;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
bannedPlayers.TryRemove(playerIndex, out _);
|
||||
playersToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in playersToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
playersToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
bannedPlayers.Clear();
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ namespace src.player.skills
|
|||
public static void NewRound()
|
||||
{
|
||||
playersInAction.Clear();
|
||||
|
||||
if (!hooked) return;
|
||||
|
||||
hooked = false;
|
||||
Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,16 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
playersFOV.TryRemove(playerIndex, out _);
|
||||
playersToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in playersToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
playersToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
foreach (var playerIndex in playersFOV.Keys)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ namespace src.player.skills
|
|||
|
||||
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
|
||||
{
|
||||
if (invisiblePlayers.IsEmpty) return;
|
||||
|
||||
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
|
||||
uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null;
|
||||
|
||||
foreach (var (info, player) in infoList)
|
||||
{
|
||||
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
|
||||
|
|
@ -86,13 +91,10 @@ namespace src.player.skills
|
|||
if (info.TransmitEntities.Contains(entity.Index))
|
||||
info.TransmitEntities.Remove(entity.Index);
|
||||
|
||||
var bombIndex = GetBombIndex(playerController);
|
||||
if (bombIndex == null) continue;
|
||||
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
|
||||
if (bombEntity == null || !bombEntity.IsValid) continue;
|
||||
if (bomb == null || bombOwnerIndex != playerController.Index) continue;
|
||||
|
||||
if (info.TransmitEntities.Contains(bombEntity.Index))
|
||||
info.TransmitEntities.Remove(bombEntity.Index);
|
||||
if (info.TransmitEntities.Contains(bomb.Index))
|
||||
info.TransmitEntities.Remove(bomb.Index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -229,18 +231,6 @@ namespace src.player.skills
|
|||
particle.AcceptInput("Start");
|
||||
}
|
||||
|
||||
private static uint? GetBombIndex(CCSPlayerController player)
|
||||
{
|
||||
var bombEntities = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").ToList();
|
||||
if (bombEntities.Count == 0) return null;
|
||||
|
||||
var bomb = bombEntities.FirstOrDefault();
|
||||
if (bomb == null) return null;
|
||||
|
||||
if (bomb.OwnerEntity.Index != player.Index) return null;
|
||||
return bomb.Index;
|
||||
}
|
||||
|
||||
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#dedede", 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 idlePercentInvisibility = .3f, float duckPercentInvisibility = .3f, float knifePercentInvisibility = .3f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
|
||||
{
|
||||
public float IdlePercentInvisibility { get; set; } = idlePercentInvisibility;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using CounterStrikeSharp.API;
|
|||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using src.utils;
|
||||
using System.Collections.Concurrent;
|
||||
using static src.jRandomSkills;
|
||||
|
||||
namespace src.player.skills
|
||||
|
|
@ -10,6 +11,9 @@ namespace src.player.skills
|
|||
{
|
||||
private const Skills skillName = Skills.NoRecoil;
|
||||
|
||||
private static readonly ConcurrentDictionary<uint, byte> holders = [];
|
||||
private static bool noSpreadActive;
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
|
|
@ -17,21 +21,33 @@ namespace src.player.skills
|
|||
|
||||
public static void NewRound()
|
||||
{
|
||||
var players = Utilities.GetPlayers();
|
||||
foreach (var player in players)
|
||||
DisableSkill(player);
|
||||
holders.Clear();
|
||||
ApplyNoSpread(false);
|
||||
}
|
||||
|
||||
public static void EnableSkill(CCSPlayerController player)
|
||||
{
|
||||
if (player == null || !player.IsValid) return;
|
||||
Server.ExecuteCommand("weapon_accuracy_nospread 1");
|
||||
|
||||
holders.TryAdd(player.Index, 0);
|
||||
ApplyNoSpread(true);
|
||||
}
|
||||
|
||||
public static void DisableSkill(CCSPlayerController player)
|
||||
{
|
||||
if (player == null || !player.IsValid) return;
|
||||
Server.ExecuteCommand("weapon_accuracy_nospread 0");
|
||||
|
||||
holders.TryRemove(player.Index, out _);
|
||||
if (holders.IsEmpty)
|
||||
ApplyNoSpread(false);
|
||||
}
|
||||
|
||||
private static void ApplyNoSpread(bool enabled)
|
||||
{
|
||||
if (noSpreadActive == enabled) return;
|
||||
|
||||
noSpreadActive = enabled;
|
||||
Server.ExecuteCommand($"weapon_accuracy_nospread {(enabled ? 1 : 0)}");
|
||||
}
|
||||
|
||||
public static void OnTick()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace src.player.skills
|
|||
private static readonly ConcurrentDictionary<uint, float> plantingPlayers = [];
|
||||
// mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override.
|
||||
private static int defaultC4Timer = 40;
|
||||
private static bool c4TimerOverridden;
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
|
|
@ -25,6 +26,7 @@ namespace src.player.skills
|
|||
public static void EnableSkill(CCSPlayerController player)
|
||||
{
|
||||
// At round start (not at plant) so the client HUD/alert countdown is right before the plant completes.
|
||||
c4TimerOverridden = true;
|
||||
Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "extraC4BlowTime")}");
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +72,9 @@ namespace src.player.skills
|
|||
DisableSkill(player);
|
||||
plantingPlayers.Clear();
|
||||
|
||||
if (!c4TimerOverridden) return;
|
||||
|
||||
c4TimerOverridden = false;
|
||||
Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,19 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"), false);
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
lock (setLock)
|
||||
{
|
||||
poisonedPlayers.TryRemove(playerIndex, out _);
|
||||
playersToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in playersToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
playersToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
lock (setLock)
|
||||
|
|
@ -29,7 +42,7 @@ namespace src.player.skills
|
|||
|
||||
public static void OnTick()
|
||||
{
|
||||
int cooldown = (int)(64 * SkillsInfo.GetValue<float>(skillName, "Cooldown"));
|
||||
int cooldown = Math.Max(1, (int)(64 * SkillsInfo.GetValue<float>(skillName, "Cooldown")));
|
||||
|
||||
if (Server.TickCount % cooldown == 0)
|
||||
{
|
||||
|
|
@ -43,7 +56,7 @@ namespace src.player.skills
|
|||
var pawn = player.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid) continue;
|
||||
|
||||
if (Jester.GetJesterInfo(playerIndex)?.Active == true) continue;
|
||||
if (Jester.IsActiveJester(playerIndex)) continue;
|
||||
|
||||
if (pawn.Health <= SkillsInfo.GetValue<int>(skillName, "MinHealth")) continue;
|
||||
SkillUtils.TakeHealth(pawn, SkillsInfo.GetValue<int>(skillName, "Damage"));
|
||||
|
|
|
|||
|
|
@ -28,6 +28,19 @@ namespace src.player.skills
|
|||
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
||||
}
|
||||
|
||||
public static void PlayerDisconnect(uint playerIndex)
|
||||
{
|
||||
lock (setLock)
|
||||
{
|
||||
bannedPlayers.TryRemove(playerIndex, out _);
|
||||
playersToTarget.TryRemove(playerIndex, out _);
|
||||
|
||||
foreach (var kvp in playersToTarget)
|
||||
if (kvp.Value == playerIndex)
|
||||
playersToTarget.TryRemove(kvp.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
lock (setLock)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ namespace src.player.skills
|
|||
public class RadarHack : ISkill
|
||||
{
|
||||
private const Skills skillName = Skills.RadarHack;
|
||||
private static readonly Skills[] hidingSkills = [Skills.Ghost, Skills.Ninja, Skills.C4Camouflage];
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
|
|
@ -43,8 +44,9 @@ namespace src.player.skills
|
|||
var enemyPawn = enemyEvent.PlayerPawn.Value;
|
||||
if (enemyPawn == null || !enemyPawn.IsValid) continue;
|
||||
|
||||
// Invisibility (low render alpha) beats the radar hack.
|
||||
if (enemyPawn.Render.A < 200) continue;
|
||||
var enemyInfo = PlayerManager.GetPlayerByIndex(enemyEvent.Index);
|
||||
if (enemyInfo != null && Array.IndexOf(hidingSkills, enemyInfo.Skill) >= 0 && enemyPawn.Render.A < 200)
|
||||
continue;
|
||||
|
||||
// Only the observer's slot bit — the Spotted bool would reveal to the whole team.
|
||||
enemyPawn.EntitySpottedState.SpottedByMask[0] |= (1u << (slot % 32));
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ namespace src.player.skills
|
|||
if (attacker == null || !attacker.IsValid) return;
|
||||
if (victimEvent == null || !victimEvent.IsValid || !victim.PawnIsAlive) return;
|
||||
|
||||
if (PlayerManager.GetPlayerByIndex(victimEvent!.Index)?.Skill != skillName) return;
|
||||
|
||||
if (SkillPlayerInfo.TryGetValue(victimEvent!.Index, out var skillInfo))
|
||||
{
|
||||
if (!skillInfo.CanUse) return;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ namespace src.player.skills
|
|||
|
||||
public static void OnTick()
|
||||
{
|
||||
if (Server.TickCount % (int)(64 * SkillsInfo.GetValue<float>(skillName, "cooldown")) != 0) return;
|
||||
int cooldown = Math.Max(1, (int)(64 * SkillsInfo.GetValue<float>(skillName, "cooldown")));
|
||||
if (Server.TickCount % cooldown != 0) return;
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
{
|
||||
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace src.player.skills
|
|||
public class SecondLife : ISkill
|
||||
{
|
||||
private const Skills skillName = Skills.SecondLife;
|
||||
private static readonly ConcurrentDictionary<nint, byte> usedThisRound = [];
|
||||
private static readonly ConcurrentDictionary<nint, int> usedThisRound = [];
|
||||
private static readonly object setLock = new();
|
||||
|
||||
public static void LoadSkill()
|
||||
|
|
@ -45,7 +45,13 @@ namespace src.player.skills
|
|||
if (victimInfo == null || victimInfo.Skill != skillName) return;
|
||||
|
||||
if (info.Damage < victimPawn.Health) return;
|
||||
if (usedThisRound.ContainsKey(victim.Handle)) return;
|
||||
|
||||
if (usedThisRound.TryGetValue(victim.Handle, out int savedTick))
|
||||
{
|
||||
if (savedTick == Server.TickCount)
|
||||
info.Damage = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
lock (setLock)
|
||||
{
|
||||
|
|
@ -54,18 +60,18 @@ namespace src.player.skills
|
|||
var spawnpoint = SkillUtils.GetSpawnPointVector(victim);
|
||||
if (spawnpoint == null) return; // no clean respawn point -> let the normal death happen
|
||||
|
||||
usedThisRound.TryAdd(victim.Handle, 0);
|
||||
usedThisRound.TryAdd(victim.Handle, Server.TickCount);
|
||||
info.Damage = 0;
|
||||
|
||||
int startHealth = SkillsInfo.GetValue<int>(skillName, "startHealth");
|
||||
victimPawn.Health = SkillsInfo.GetValue<int>(skillName, "startHealth");
|
||||
Utilities.SetStateChanged(victimPawn, "CBaseEntity", "m_iHealth");
|
||||
|
||||
Server.NextFrame(() =>
|
||||
{
|
||||
if (victim == null || !victim.IsValid) return;
|
||||
var pawn = victim.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid) return;
|
||||
|
||||
pawn.Health = startHealth;
|
||||
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
|
||||
pawn.Teleport(spawnpoint, null, new Vector(0, 0, 0));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace src.player.skills
|
|||
private const Skills skillName = Skills.ShortBomb;
|
||||
// mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override.
|
||||
private static int defaultC4Timer = 40;
|
||||
private static bool c4TimerOverridden;
|
||||
|
||||
public static void LoadSkill()
|
||||
{
|
||||
|
|
@ -22,11 +23,15 @@ namespace src.player.skills
|
|||
public static void EnableSkill(CCSPlayerController player)
|
||||
{
|
||||
// At round start (not at plant) so the client HUD/alert countdown is right before the plant completes.
|
||||
c4TimerOverridden = true;
|
||||
Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "detonationTime")}");
|
||||
}
|
||||
|
||||
public static void NewRound()
|
||||
{
|
||||
if (!c4TimerOverridden) return;
|
||||
|
||||
c4TimerOverridden = false;
|
||||
Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,20 +59,20 @@ namespace src.player.skills
|
|||
foreach (var player in Utilities.GetPlayers())
|
||||
if (cameras.TryGetValue(player.Index, out var cameraInfo) && cameraInfo.Item2 != 0)
|
||||
{
|
||||
if (player == null || !player.IsValid) return;
|
||||
if (player == null || !player.IsValid) continue;
|
||||
|
||||
var enemy = Utilities.GetPlayerFromIndex((int)cameraInfo.Item3);
|
||||
if (enemy == null || !enemy.IsValid || enemy.PlayerPawn == null)
|
||||
{
|
||||
ChangeCamera(player, true);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
var enemyPawn = enemy.PlayerPawn.Value;
|
||||
if (enemyPawn == null || !enemyPawn.IsValid)
|
||||
{
|
||||
ChangeCamera(player, true);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enemyPawn.Health <= 0 || (player.PlayerPawn?.Value != null && player.PlayerPawn.Value.Health <= 0))
|
||||
|
|
@ -117,6 +117,10 @@ namespace src.player.skills
|
|||
pawn.CameraServices.ViewEntity.Raw = orginalCameraRaw;
|
||||
|
||||
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_pCameraServices");
|
||||
|
||||
if (forceToDefault && cameras.TryGetValue(player.Index, out var current) && current.Item2 != 0)
|
||||
cameras[player.Index] = (current.Item1, 0, current.Item3);
|
||||
|
||||
BlockWeapon(player, !defaultCam);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ namespace src.player.skills
|
|||
|
||||
public static void OnTick()
|
||||
{
|
||||
int tick = SkillsInfo.GetValue<int>(skillName, "tickCooldown");
|
||||
int tick = Math.Max(1, SkillsInfo.GetValue<int>(skillName, "tickCooldown"));
|
||||
if (Server.TickCount % tick != 0) return;
|
||||
|
||||
float smokeRadius = SkillsInfo.GetValue<float>(skillName, "smokeRadius");
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ namespace src.player.skills
|
|||
{
|
||||
lock (setLock)
|
||||
{
|
||||
if (infectedPlayers.IsEmpty && playersToTarget.IsEmpty) return;
|
||||
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
DisableSkill(player);
|
||||
|
||||
|
|
|
|||
|
|
@ -296,8 +296,8 @@ namespace src.utils
|
|||
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
|
||||
if (playerInfo == null) return false;
|
||||
|
||||
var jester = Jester.GetJesterInfo(player.Index);
|
||||
if (jester?.Active == true) return false;
|
||||
if (playerInfo.Skill == Skills.Jester && Jester.GetJesterInfo(player.Index)?.Active == true)
|
||||
return false;
|
||||
|
||||
if (playerInfo.Skill == Skills.GodMode && GodMode.HaveHodMode(player.Index))
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue